@magentrix-corp/magentrix-cli 1.3.12 → 1.3.13

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.
@@ -435,11 +435,23 @@ async function buildFromVueProject(options) {
435
435
  // This ensures pull doesn't overwrite our staged files
436
436
  console.log();
437
437
  console.log(chalk.gray('Checking workspace sync status...'));
438
+
439
+ // Check for a previous incomplete pull first — `magentrix status` only checks
440
+ // code entities, so a partial pull that synced code but not assets would falsely
441
+ // report "in sync". The marker file catches this case.
442
+ const workspaceConfig = new Config({ projectDir: workspacePath });
443
+ const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
444
+
438
445
  const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
446
+ const needsPull = syncStatus.needsPull || !!hadIncompletePull;
439
447
 
440
- if (syncStatus.needsPull) {
448
+ if (needsPull) {
441
449
  console.log();
442
- console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
450
+ if (hadIncompletePull && !syncStatus.needsPull) {
451
+ console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
452
+ } else {
453
+ console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
454
+ }
443
455
 
444
456
  const shouldPull = await confirm({
445
457
  message: 'Would you like to pull latest changes first?',
@@ -451,9 +463,16 @@ async function buildFromVueProject(options) {
451
463
  console.log(chalk.blue('Running pull from workspace...'));
452
464
  console.log();
453
465
 
466
+ // Mark pull as in-progress before starting
467
+ workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
468
+
454
469
  const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
455
470
 
456
- if (!pullSuccess) {
471
+ if (pullSuccess) {
472
+ // Pull completed successfully — clear the marker
473
+ workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
474
+ } else {
475
+ // Pull failed or was cancelled — marker stays for next run
457
476
  console.log();
458
477
  console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
459
478
 
@@ -199,9 +199,14 @@ const handleDeleteStaticAssetAction = async (instanceUrl, apiKey, action) => {
199
199
 
200
200
  if (isNotFound) {
201
201
  // Clean up base.json since file doesn't exist on server
202
- for (const name of action.names) {
203
- const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
204
- removeFromBase(filePath);
202
+ // Use the original base.json key if available (avoids path format mismatches)
203
+ if (action.baseKey) {
204
+ removeFromBase(action.baseKey);
205
+ } else {
206
+ for (const name of action.names) {
207
+ const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
208
+ removeFromBase(filePath);
209
+ }
205
210
  }
206
211
  return { cleanedFromCache: true };
207
212
  }
@@ -253,7 +258,8 @@ const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
253
258
 
254
259
  if (isNotFound) {
255
260
  // Clean up base.json since folder doesn't exist on server
256
- removeFromBase(action.folderPath);
261
+ // Use original base.json key if available (avoids path format mismatches)
262
+ removeFromBase(action.baseKey || action.folderPath);
257
263
 
258
264
  // Also remove all files and subfolders inside this folder from base
259
265
  const hits = await config.searchObject({}, { filename: "base.json", global: false });
@@ -289,14 +295,19 @@ const handlePublishIrisAppAction = async (instanceUrl, apiKey, action) => {
289
295
  // Create zip from the app folder
290
296
  const zipBuffer = await createIrisZip(action.appPath, action.slug);
291
297
 
292
- // Publish via API with app metadata
298
+ // For updates, don't send app-name/description/icon to avoid triggering
299
+ // metadata modification that may require permissions the user doesn't have.
300
+ // The server already has the correct metadata from the original create.
301
+ // For creates, send all metadata so the app is properly registered.
302
+ const isUpdate = action.action === 'update_iris_app';
303
+
293
304
  const response = await publishApp(
294
305
  instanceUrl,
295
306
  apiKey,
296
307
  zipBuffer,
297
308
  `${action.slug}.zip`,
298
- action.appName,
299
- {
309
+ isUpdate ? null : action.appName,
310
+ isUpdate ? {} : {
300
311
  appDescription: action.appDescription,
301
312
  appIconId: action.appIconId
302
313
  }
@@ -425,9 +436,14 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
425
436
  case "delete_static_asset":
426
437
  // Skip if already cleaned from cache during 404 handling
427
438
  if (!operationResult?.cleanedFromCache) {
428
- for (const name of action.names) {
429
- const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
430
- removeFromBase(filePath);
439
+ // Use the original base.json key if available (avoids path format mismatches)
440
+ if (action.baseKey) {
441
+ removeFromBase(action.baseKey);
442
+ } else {
443
+ for (const name of action.names) {
444
+ const filePath = action.filePath || `${EXPORT_ROOT}/${action.folder}/${name}`;
445
+ removeFromBase(filePath);
446
+ }
431
447
  }
432
448
  }
433
449
  break;
@@ -443,8 +459,8 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
443
459
  case "delete_folder": {
444
460
  // Skip if already cleaned from cache during 404 handling
445
461
  if (!operationResult?.cleanedFromCache) {
446
- // Remove the folder itself from base using recordId (which is the folderPath)
447
- removeFromBase(action.folderPath);
462
+ // Remove the folder itself from base using the original base.json key
463
+ removeFromBase(action.baseKey || action.folderPath);
448
464
 
449
465
  // Also remove all files and subfolders inside this folder from base
450
466
  const hits = await config.searchObject({}, { filename: "base.json", global: false });
@@ -1029,7 +1045,8 @@ export const runPublish = async (options = {}) => {
1029
1045
  action: "delete_folder",
1030
1046
  folderPath: cachedPath,
1031
1047
  parentPath: toApiFolderPath(parentDir),
1032
- folderName: path.basename(cachedPath)
1048
+ folderName: path.basename(cachedPath),
1049
+ baseKey: cachedFolder.tag // The original base.json key for correct cache cleanup
1033
1050
  });
1034
1051
  }
1035
1052
  }
@@ -1067,7 +1084,8 @@ export const runPublish = async (options = {}) => {
1067
1084
  action: 'delete_static_asset',
1068
1085
  folder: toApiPath(actualPath),
1069
1086
  names: [path.basename(actualPath)],
1070
- filePath: actualPath // Store actual file path for filtering
1087
+ filePath: actualPath, // Store actual file path for filtering
1088
+ baseKey: id // The original base.json key for correct cache cleanup
1071
1089
  });
1072
1090
  continue;
1073
1091
  }
package/actions/pull.js CHANGED
@@ -128,9 +128,12 @@ export const pull = async () => {
128
128
  progress.startStep('load');
129
129
  const hits = await config.searchObject({}, { filename: "base.json", global: false });
130
130
  const cachedResults = hits?.[0]?.value || {};
131
- const cachedAssets = Object.values(cachedResults).filter(c =>
132
- c.type === 'File' || c.type === 'Folder'
133
- );
131
+ // Include the base.json key (_baseKey) alongside each entry so we can
132
+ // use the correct key for removeFromBaseBulk later. The keys may differ
133
+ // from lastKnownActualPath/filePath due to path format inconsistencies.
134
+ const cachedAssets = Object.entries(cachedResults)
135
+ .filter(([, c]) => c.type === 'File' || c.type === 'Folder')
136
+ .map(([key, c]) => ({ ...c, _baseKey: key }));
134
137
  progress.completeStep('load', `✓ Loaded ${Object.keys(cachedResults).length} cached entries`);
135
138
 
136
139
  // Step 4a: Download code entities (ActiveClass and ActivePage)
@@ -338,7 +341,7 @@ export const pull = async () => {
338
341
 
339
342
  // If this asset was in base.json but not returned from server, it was deleted
340
343
  if (!serverAssetPaths.has(cachedPath)) {
341
- assetsToDelete.push(cachedPath);
344
+ assetsToDelete.push({ path: cachedPath, baseKey: cached._baseKey });
342
345
  }
343
346
 
344
347
  processedCount++;
@@ -402,33 +405,35 @@ export const pull = async () => {
402
405
  // Delete local files/folders that were deleted on server
403
406
  logger.info(`Starting deletion of ${assetsToDelete.length} assets`);
404
407
  const deletionLogs = [];
405
- for (const assetPath of assetsToDelete) {
408
+ // Track base.json keys for entries that need removal
409
+ const assetBaseKeysToRemove = [];
410
+ for (const asset of assetsToDelete) {
406
411
  try {
407
- if (fs.existsSync(assetPath)) {
408
- const stats = fs.statSync(assetPath);
412
+ if (fs.existsSync(asset.path)) {
413
+ const stats = fs.statSync(asset.path);
409
414
  if (stats.isDirectory()) {
410
- fs.rmSync(assetPath, { recursive: true, force: true });
411
- deletionLogs.push({ type: 'folder', path: assetPath });
412
- logger.info('Deleted folder', { path: assetPath });
415
+ fs.rmSync(asset.path, { recursive: true, force: true });
416
+ deletionLogs.push({ type: 'folder', path: asset.path });
417
+ logger.info('Deleted folder', { path: asset.path });
413
418
  } else {
414
- fs.unlinkSync(assetPath);
415
- deletionLogs.push({ type: 'file', path: assetPath });
416
- logger.info('Deleted file', { path: assetPath });
419
+ fs.unlinkSync(asset.path);
420
+ deletionLogs.push({ type: 'file', path: asset.path });
421
+ logger.info('Deleted file', { path: asset.path });
417
422
  }
418
423
  }
424
+ // Always remove from base.json regardless of whether the local file
425
+ // existed — the server confirmed deletion, so the cache entry is stale.
426
+ assetBaseKeysToRemove.push(asset.baseKey);
419
427
  } catch (err) {
420
- deletionLogs.push({ type: 'error', path: assetPath, error: err.message });
421
- logger.error(`Failed to delete asset: ${assetPath}`, err);
428
+ deletionLogs.push({ type: 'error', path: asset.path, error: err.message });
429
+ logger.error(`Failed to delete asset: ${asset.path}`, err);
422
430
  }
423
431
  }
424
432
 
425
- // Bulk remove from base.json
426
- const assetPathsToRemove = deletionLogs
427
- .filter(l => l.type === 'file' || l.type === 'folder')
428
- .map(l => l.path);
429
-
430
- if (assetPathsToRemove.length > 0) {
431
- removeFromBaseBulk(assetPathsToRemove);
433
+ // Bulk remove from base.json using the original base.json keys
434
+ // (not the normalized paths, which may have a different format)
435
+ if (assetBaseKeysToRemove.length > 0) {
436
+ removeFromBaseBulk(assetBaseKeysToRemove);
432
437
  }
433
438
 
434
439
  // Delete local code entity files that were deleted on server
@@ -639,6 +644,10 @@ export const pull = async () => {
639
644
  logger.close();
640
645
  progress.finish('Pull completed successfully!');
641
646
 
647
+ // Clear any incomplete-pull marker (set by vue-run-build when a pull is
648
+ // started but cancelled/fails). A successful pull means we're fully synced.
649
+ config.removeKey('pullIncomplete', { filename: 'config.json' });
650
+
642
651
  // Summary
643
652
  console.log(chalk.bold(`Summary:`));
644
653
  console.log(` • ${activeClassResult.Records.length} ActiveClass records`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.3.12",
3
+ "version": "1.3.13",
4
4
  "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/utils/cacher.js CHANGED
@@ -8,6 +8,9 @@ import { EXPORT_ROOT, ALLOWED_SRC_DIRS, IRIS_APPS_DIR } from '../vars/global.js'
8
8
 
9
9
  const config = new Config();
10
10
 
11
+ // System/hidden files that should never be synced
12
+ const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
13
+
11
14
  /**
12
15
  * Recursively caches all files in a directory with tagging and content snapshotting
13
16
  * OPTIMIZED: Parallel processing + mtime checks for maximum speed
@@ -179,6 +182,11 @@ export async function walkFiles(dir, settings) {
179
182
  for (const entry of entries) {
180
183
  const fullPath = path.join(dir, entry.name);
181
184
 
185
+ // Skip OS-generated system files (.DS_Store, Thumbs.db, etc.)
186
+ if (IGNORED_FILES.has(entry.name)) {
187
+ continue;
188
+ }
189
+
182
190
  // Check if this path should be ignored
183
191
  if (ignore.find(p => fullPath.startsWith(p) || fullPath === p)) {
184
192
  continue;
@@ -5,6 +5,9 @@ import { randomUUID, createHash } from 'node:crypto';
5
5
  import archiver from 'archiver';
6
6
  import extractZip from 'extract-zip';
7
7
 
8
+ // System/hidden files that should never be included in IRIS app zips or hashes
9
+ const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
10
+
8
11
  /**
9
12
  * Recursively fix permissions on extracted files.
10
13
  * Sets directories to 0o755 and files to 0o644.
@@ -61,7 +64,10 @@ export async function createIrisZip(distPath, appSlug) {
61
64
 
62
65
  // Add the dist directory contents under the app slug folder
63
66
  // This creates the structure: appSlug/remoteEntry.js, appSlug/assets/...
64
- archive.directory(distPath, appSlug);
67
+ // Filter out OS-generated system files (.DS_Store, Thumbs.db, etc.)
68
+ archive.directory(distPath, appSlug, (entry) =>
69
+ IGNORED_FILES.has(basename(entry.name)) ? false : entry
70
+ );
65
71
 
66
72
  archive.finalize();
67
73
  });
@@ -195,6 +201,9 @@ export function getFilesRecursive(dir, basePath = dir) {
195
201
  function walk(currentDir) {
196
202
  const entries = readdirSync(currentDir);
197
203
  for (const entry of entries) {
204
+ // Skip OS-generated system files
205
+ if (IGNORED_FILES.has(entry)) continue;
206
+
198
207
  const fullPath = join(currentDir, entry);
199
208
  const stat = statSync(fullPath);
200
209
  if (stat.isDirectory()) {
@@ -48,10 +48,6 @@ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appNam
48
48
  throw new Error('filename is required');
49
49
  }
50
50
 
51
- if (!appName) {
52
- throw new Error('appName is required for navigation menu updates');
53
- }
54
-
55
51
  // Create a File object from the buffer for FormData
56
52
  const file = new File([zipBuffer], filename, { type: 'application/zip' });
57
53
 
@@ -59,8 +55,14 @@ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appNam
59
55
  formData.append('file', file);
60
56
 
61
57
  // Build query parameters
58
+ // Note: app-name is only sent when provided (required for creates, optional for updates)
59
+ // Sending app-name on updates can trigger a rename attempt that fails if the user
60
+ // lacks metadata-edit permissions on the server
62
61
  const params = new URLSearchParams();
63
- params.append('app-name', appName);
62
+
63
+ if (appName) {
64
+ params.append('app-name', appName);
65
+ }
64
66
 
65
67
  if (options.appDescription) {
66
68
  params.append('app-description', options.appDescription);