@magentrix-corp/magentrix-cli 1.3.10 → 1.3.12

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.
@@ -27,9 +27,33 @@ import {
27
27
  } from '../../utils/iris/linker.js';
28
28
  import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
29
29
  import { getValidWorkspaces } from '../../utils/workspaces.js';
30
+ import { formatTimeoutError } from '../../utils/iris/errors.js';
30
31
 
31
32
  const config = new Config();
32
33
 
34
+ /**
35
+ * Timeout for status check operations (30 seconds).
36
+ */
37
+ const STATUS_CHECK_TIMEOUT = 30000;
38
+
39
+ /**
40
+ * Timeout for command execution (5 minutes).
41
+ */
42
+ const COMMAND_TIMEOUT = 5 * 60 * 1000;
43
+
44
+ /**
45
+ * Display warnings from build/stage operations.
46
+ *
47
+ * @param {string[]} warnings - Array of warning messages
48
+ */
49
+ function displayWarnings(warnings) {
50
+ if (!warnings || warnings.length === 0) return;
51
+
52
+ for (const warning of warnings) {
53
+ console.log(chalk.yellow(`⚠ ${warning}`));
54
+ }
55
+ }
56
+
33
57
  /**
34
58
  * Check if the current directory is a Vue project (has config.ts).
35
59
  * @returns {boolean}
@@ -221,6 +245,9 @@ export const vueBuildStage = async (options = {}) => {
221
245
  console.log(chalk.green(`\u2713 Build completed successfully`));
222
246
  console.log(chalk.gray(` Output: ${distPath}`));
223
247
 
248
+ // Display any build warnings
249
+ displayWarnings(buildResult.warnings);
250
+
224
251
  // Validate build output
225
252
  const validation = validateIrisBuild(distPath);
226
253
  if (!validation.valid) {
@@ -245,6 +272,9 @@ export const vueBuildStage = async (options = {}) => {
245
272
 
246
273
  console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
247
274
 
275
+ // Display any staging warnings
276
+ displayWarnings(stageResult.warnings);
277
+
248
278
  // Summary
249
279
  console.log();
250
280
  console.log(chalk.green('─'.repeat(48)));
@@ -387,6 +417,9 @@ async function buildFromVueProject(options) {
387
417
  console.log(chalk.green(`\u2713 Build completed successfully`));
388
418
  console.log(chalk.gray(` Output: ${distPath}`));
389
419
 
420
+ // Display any build warnings
421
+ displayWarnings(buildResult.warnings);
422
+
390
423
  // Validate build output
391
424
  const validation = validateIrisBuild(distPath);
392
425
  if (!validation.valid) {
@@ -469,6 +502,9 @@ async function buildFromVueProject(options) {
469
502
 
470
503
  console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
471
504
 
505
+ // Display any staging warnings
506
+ displayWarnings(stageResult.warnings);
507
+
472
508
  // Summary
473
509
  console.log();
474
510
  console.log(chalk.green('─'.repeat(48)));
@@ -509,7 +545,7 @@ async function buildFromVueProject(options) {
509
545
  * Check if a workspace needs to pull (has remote changes or conflicts).
510
546
  *
511
547
  * @param {string} workspacePath - Path to the Magentrix workspace
512
- * @returns {Promise<{checked: boolean, needsPull: boolean}>}
548
+ * @returns {Promise<{checked: boolean, needsPull: boolean, timedOut: boolean}>}
513
549
  */
514
550
  async function checkWorkspaceSyncStatus(workspacePath) {
515
551
  return new Promise((resolvePromise) => {
@@ -517,6 +553,7 @@ async function checkWorkspaceSyncStatus(workspacePath) {
517
553
  const npmCmd = isWindows ? 'npx.cmd' : 'npx';
518
554
 
519
555
  let output = '';
556
+ let resolved = false;
520
557
 
521
558
  const child = spawn(npmCmd, ['magentrix', 'status'], {
522
559
  cwd: workspacePath,
@@ -524,6 +561,20 @@ async function checkWorkspaceSyncStatus(workspacePath) {
524
561
  shell: isWindows
525
562
  });
526
563
 
564
+ // Set timeout for status check
565
+ const timeout = setTimeout(() => {
566
+ if (!resolved) {
567
+ resolved = true;
568
+ try {
569
+ child.kill('SIGTERM');
570
+ } catch {
571
+ // Ignore kill errors
572
+ }
573
+ console.log(chalk.yellow(`\n⚠ Status check timed out after ${STATUS_CHECK_TIMEOUT / 1000} seconds`));
574
+ resolvePromise({ checked: false, needsPull: false, timedOut: true });
575
+ }
576
+ }, STATUS_CHECK_TIMEOUT);
577
+
527
578
  child.stdout.on('data', (data) => {
528
579
  output += data.toString();
529
580
  });
@@ -533,6 +584,10 @@ async function checkWorkspaceSyncStatus(workspacePath) {
533
584
  });
534
585
 
535
586
  child.on('close', (code) => {
587
+ if (resolved) return;
588
+ resolved = true;
589
+ clearTimeout(timeout);
590
+
536
591
  // Check output for specific issue indicators from status command
537
592
  // Note: We avoid checking for "remote" as it appears in normal output
538
593
  // ("Checking local files vs remote Magentrix...")
@@ -552,12 +607,17 @@ async function checkWorkspaceSyncStatus(workspacePath) {
552
607
 
553
608
  const needsPull = !isInSync && (hasConflict || isOutdated || isAhead || isMissing || hasContentMismatch || hasWarnings);
554
609
 
555
- resolvePromise({ checked: code === 0, needsPull });
610
+ resolvePromise({ checked: code === 0, needsPull, timedOut: false });
556
611
  });
557
612
 
558
- child.on('error', () => {
613
+ child.on('error', (err) => {
614
+ if (resolved) return;
615
+ resolved = true;
616
+ clearTimeout(timeout);
617
+
559
618
  // If we can't check, assume it's fine and let them proceed
560
- resolvePromise({ checked: false, needsPull: false });
619
+ console.log(chalk.yellow(`\n⚠ Could not check sync status: ${err.message}`));
620
+ resolvePromise({ checked: false, needsPull: false, timedOut: false });
561
621
  });
562
622
  });
563
623
  }
@@ -567,24 +627,52 @@ async function checkWorkspaceSyncStatus(workspacePath) {
567
627
  *
568
628
  * @param {string} workspacePath - Path to the Magentrix workspace
569
629
  * @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
630
+ * @param {number} timeout - Optional timeout in milliseconds (defaults to COMMAND_TIMEOUT)
570
631
  * @returns {Promise<boolean>} - True if command succeeded
571
632
  */
572
- async function runCommandFromWorkspace(workspacePath, command) {
633
+ async function runCommandFromWorkspace(workspacePath, command, timeout = COMMAND_TIMEOUT) {
573
634
  return new Promise((resolvePromise) => {
574
635
  const isWindows = process.platform === 'win32';
575
636
  const npmCmd = isWindows ? 'npx.cmd' : 'npx';
576
637
 
638
+ let resolved = false;
639
+
577
640
  const child = spawn(npmCmd, ['magentrix', command], {
578
641
  cwd: workspacePath,
579
642
  stdio: 'inherit',
580
643
  shell: isWindows
581
644
  });
582
645
 
646
+ // Set timeout for command execution
647
+ const timeoutId = setTimeout(() => {
648
+ if (!resolved) {
649
+ resolved = true;
650
+ try {
651
+ child.kill('SIGTERM');
652
+ } catch {
653
+ // Ignore kill errors
654
+ }
655
+ console.log();
656
+ console.log(chalk.red(formatTimeoutError({
657
+ operation: `magentrix ${command}`,
658
+ timeout: timeout,
659
+ suggestion: `The ${command} operation is taking too long. Check your network connection or try again later.`
660
+ })));
661
+ resolvePromise(false);
662
+ }
663
+ }, timeout);
664
+
583
665
  child.on('close', (code) => {
666
+ if (resolved) return;
667
+ resolved = true;
668
+ clearTimeout(timeoutId);
584
669
  resolvePromise(code === 0);
585
670
  });
586
671
 
587
672
  child.on('error', (err) => {
673
+ if (resolved) return;
674
+ resolved = true;
675
+ clearTimeout(timeoutId);
588
676
  console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
589
677
  resolvePromise(false);
590
678
  });
@@ -8,6 +8,8 @@ import { getLinkedProjects, unlinkVueProject } from '../../utils/iris/linker.js'
8
8
  import { deleteIrisAppFromServer, deleteLocalIrisAppFiles } from '../../utils/iris/deleteHelper.js';
9
9
  import { showPermissionError } from '../../utils/permissionError.js';
10
10
  import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
11
+ import { acquireLock, releaseLock, LockTypes } from '../../utils/iris/lock.js';
12
+ import { formatFileLockError } from '../../utils/iris/errors.js';
11
13
 
12
14
  const config = new Config();
13
15
 
@@ -99,6 +101,42 @@ export const irisDelete = async () => {
99
101
  return;
100
102
  }
101
103
 
104
+ // Acquire delete lock to prevent concurrent deletions
105
+ const lockBasePath = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
106
+ const lockResult = acquireLock(LockTypes.DELETE, {
107
+ context: slug,
108
+ operation: `deleting ${slug}`,
109
+ basePath: lockBasePath
110
+ });
111
+
112
+ // Track if lock was acquired (permission errors are non-fatal for delete)
113
+ let lockAcquired = lockResult.acquired;
114
+ if (!lockResult.acquired) {
115
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
116
+ console.log(chalk.yellow('Warning: Could not create delete lock (permission issue). Proceeding without lock.'));
117
+ lockAcquired = false;
118
+ } else {
119
+ console.log(chalk.red('Cannot delete app:'));
120
+ console.log(chalk.yellow(lockResult.error));
121
+ return;
122
+ }
123
+ }
124
+
125
+ try {
126
+ await performDelete(slug, appName);
127
+ } finally {
128
+ if (lockAcquired) {
129
+ releaseLock(LockTypes.DELETE, { context: slug, basePath: lockBasePath });
130
+ }
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Perform the actual delete operation.
136
+ * @param {string} slug - App slug
137
+ * @param {string} appName - App display name
138
+ */
139
+ async function performDelete(slug, appName) {
102
140
  // Check if app is linked
103
141
  const linkedProjects = getLinkedProjects();
104
142
  const linkedProject = linkedProjects.find(p => p.slug === slug);
@@ -176,6 +214,13 @@ export const irisDelete = async () => {
176
214
  console.log(chalk.gray('Note: The app was deleted from the server and cache.'));
177
215
  console.log(chalk.gray('Only local file cleanup failed.'));
178
216
  console.log();
217
+ } else if (localDeleteResult.isFileLocked) {
218
+ console.log(chalk.yellow('⚠ Could not delete local files - files are in use'));
219
+ console.log(chalk.gray(localDeleteResult.error));
220
+ console.log();
221
+ console.log(chalk.gray('Note: The app was deleted from the server and cache.'));
222
+ console.log(chalk.gray('Close any programs using these files and delete manually.'));
223
+ console.log();
179
224
  } else {
180
225
  console.log(chalk.yellow(`⚠ Failed to delete local files: ${localDeleteResult.error}`));
181
226
  console.log(chalk.gray(`Path: ${appPath}`));
@@ -206,6 +251,6 @@ export const irisDelete = async () => {
206
251
  console.log(chalk.white(` Backup saved to: ${chalk.gray(backupResult.backupPath)}`));
207
252
  console.log(chalk.white(` To restore, run: ${chalk.cyan('magentrix iris-app-recover')}`));
208
253
  console.log();
209
- };
254
+ }
210
255
 
211
256
  export default irisDelete;
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { select, input } from '@inquirer/prompts';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { existsSync } from 'node:fs';
5
- import { resolve } from 'node:path';
5
+ import { resolve, basename, join } from 'node:path';
6
6
  import {
7
7
  readVueConfig,
8
8
  formatMissingConfigError,
@@ -16,6 +16,21 @@ import {
16
16
  getLinkedProjectsWithStatus,
17
17
  buildProjectChoices
18
18
  } from '../../utils/iris/linker.js';
19
+ import {
20
+ acquireLock,
21
+ releaseLock,
22
+ LockTypes,
23
+ createProjectContext
24
+ } from '../../utils/iris/lock.js';
25
+ import {
26
+ formatNetworkError,
27
+ formatError
28
+ } from '../../utils/iris/errors.js';
29
+
30
+ /**
31
+ * Timeout for fetching assets (30 seconds).
32
+ */
33
+ const ASSET_FETCH_TIMEOUT = 30000;
19
34
 
20
35
  /**
21
36
  * vue-run-dev command - Start Vue dev server with platform assets injected.
@@ -95,8 +110,17 @@ export const irisDev = async (options = {}) => {
95
110
 
96
111
  try {
97
112
  // Get access token using the refresh token from .env.development
98
- const tokenData = await getAccessToken(vueConfig.refreshToken, siteUrl);
99
- const assetsResult = await getIrisAssets(siteUrl, tokenData.token);
113
+ // Use Promise.race to implement timeout
114
+ const tokenPromise = getAccessToken(vueConfig.refreshToken, siteUrl);
115
+ const timeoutPromise = new Promise((_, reject) =>
116
+ setTimeout(() => reject(new Error('Request timed out')), ASSET_FETCH_TIMEOUT)
117
+ );
118
+
119
+ const tokenData = await Promise.race([tokenPromise, timeoutPromise]);
120
+
121
+ // Fetch assets with timeout
122
+ const assetsPromise = getIrisAssets(siteUrl, tokenData.token);
123
+ const assetsResult = await Promise.race([assetsPromise, timeoutPromise]);
100
124
 
101
125
  if (assetsResult.success && assetsResult.assets?.length > 0) {
102
126
  console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
@@ -115,7 +139,12 @@ export const irisDev = async (options = {}) => {
115
139
  if (injectResult.success) {
116
140
  console.log(chalk.green(`\u2713 Assets updated in ${injectResult.targetName}`));
117
141
  } else {
118
- console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
142
+ // Show specific error if available
143
+ if (injectResult.error) {
144
+ console.log(chalk.yellow(`Warning: Could not inject assets: ${injectResult.error}`));
145
+ } else {
146
+ console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
147
+ }
119
148
  }
120
149
  }
121
150
  } else if (assetsResult.error) {
@@ -125,7 +154,23 @@ export const irisDev = async (options = {}) => {
125
154
  console.log(chalk.yellow('No platform assets found.'));
126
155
  }
127
156
  } catch (err) {
128
- console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
157
+ // Provide more helpful error messages based on error type
158
+ if (err.message === 'Request timed out') {
159
+ console.log(chalk.yellow('Warning: Asset fetch timed out.'));
160
+ console.log(chalk.gray('Check your network connection or try again later.'));
161
+ } else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
162
+ console.log(chalk.yellow(formatNetworkError({
163
+ operation: 'fetch assets',
164
+ url: siteUrl,
165
+ error: err
166
+ })));
167
+ } else if (err.message?.includes('401') || err.message?.includes('Unauthorized')) {
168
+ console.log(chalk.yellow('Warning: Authentication failed.'));
169
+ console.log(chalk.gray('Your VITE_REFRESH_TOKEN may be invalid or expired.'));
170
+ console.log(chalk.gray('Get a new API key from your Magentrix platform.'));
171
+ } else {
172
+ console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
173
+ }
129
174
  console.log(chalk.gray('Continuing without asset injection.'));
130
175
  }
131
176
  }
@@ -147,8 +192,34 @@ export const irisDev = async (options = {}) => {
147
192
 
148
193
  /**
149
194
  * Run the Vue development server.
195
+ *
196
+ * @param {string} projectPath - Path to the Vue project
197
+ * @returns {Promise<number>} - Exit code
150
198
  */
151
199
  async function runDevServer(projectPath) {
200
+ // Acquire dev server lock to prevent multiple instances
201
+ // Use user home for consistent lock location across projects
202
+ const lockContext = createProjectContext(projectPath);
203
+ const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
204
+ const lockResult = acquireLock(LockTypes.DEV_SERVER, {
205
+ context: lockContext,
206
+ operation: `dev server for ${basename(projectPath)}`,
207
+ basePath: lockBasePath
208
+ });
209
+
210
+ // Track if lock was acquired (permission errors are non-fatal)
211
+ let lockAcquired = lockResult.acquired;
212
+ if (!lockResult.acquired) {
213
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
214
+ console.log(chalk.yellow('Warning: Could not create dev server lock (permission issue). Proceeding without lock.'));
215
+ lockAcquired = false;
216
+ } else {
217
+ console.log(chalk.red('Cannot start dev server:'));
218
+ console.log(chalk.yellow(lockResult.error));
219
+ return 1;
220
+ }
221
+ }
222
+
152
223
  return new Promise((resolvePromise) => {
153
224
  const isWindows = process.platform === 'win32';
154
225
  const npmCmd = isWindows ? 'npm.cmd' : 'npm';
@@ -159,16 +230,38 @@ async function runDevServer(projectPath) {
159
230
  shell: isWindows // Windows requires shell: true for .cmd files
160
231
  });
161
232
 
162
- // Handle process signals
163
- process.on('SIGINT', () => {
164
- child.kill('SIGINT');
165
- });
166
-
167
- process.on('SIGTERM', () => {
168
- child.kill('SIGTERM');
233
+ // Cleanup function to release lock
234
+ const cleanup = () => {
235
+ if (lockAcquired) {
236
+ releaseLock(LockTypes.DEV_SERVER, { context: lockContext, basePath: lockBasePath });
237
+ }
238
+ };
239
+
240
+ // Handle process signals - use once() to avoid memory leaks
241
+ const handleSignal = (signal) => {
242
+ cleanup();
243
+ child.kill(signal);
244
+ // Give child process a moment to exit, then force exit
245
+ setTimeout(() => {
246
+ process.exit(0);
247
+ }, 500);
248
+ };
249
+
250
+ const sigintHandler = () => handleSignal('SIGINT');
251
+ const sigtermHandler = () => handleSignal('SIGTERM');
252
+
253
+ process.once('SIGINT', sigintHandler);
254
+ process.once('SIGTERM', sigtermHandler);
255
+
256
+ // Remove handlers when child exits to avoid memory leaks
257
+ child.on('exit', () => {
258
+ process.removeListener('SIGINT', sigintHandler);
259
+ process.removeListener('SIGTERM', sigtermHandler);
169
260
  });
170
261
 
171
262
  child.on('close', (code) => {
263
+ cleanup();
264
+
172
265
  // If dev server exited with error, show helpful context
173
266
  if (code !== 0 && code !== null) {
174
267
  console.log();
@@ -184,7 +277,13 @@ async function runDevServer(projectPath) {
184
277
  console.log(chalk.gray(' • Syntax errors in project files'));
185
278
  console.log();
186
279
  console.log(chalk.white('If you see EACCES/permission errors, try:'));
187
- console.log(chalk.cyan(` sudo chown -R $(whoami) "${projectPath}/node_modules"`));
280
+ if (isWindows) {
281
+ console.log(chalk.cyan(' Run terminal as Administrator'));
282
+ console.log(chalk.cyan(` rd /s /q "${projectPath}\\node_modules"`));
283
+ console.log(chalk.cyan(' npm install'));
284
+ } else {
285
+ console.log(chalk.cyan(` sudo chown -R $(whoami) "${projectPath}/node_modules"`));
286
+ }
188
287
  console.log(chalk.yellow('─'.repeat(48)));
189
288
  }
190
289
 
@@ -192,7 +291,24 @@ async function runDevServer(projectPath) {
192
291
  });
193
292
 
194
293
  child.on('error', (err) => {
294
+ cleanup();
195
295
  console.log(chalk.red(`Failed to start dev server: ${err.message}`));
296
+
297
+ // Provide more specific error messages
298
+ if (err.code === 'ENOENT') {
299
+ console.log();
300
+ console.log(chalk.yellow('npm was not found on your system.'));
301
+ console.log(chalk.gray('Make sure Node.js is installed and npm is in your PATH.'));
302
+ } else if (err.code === 'EACCES' || err.code === 'EPERM') {
303
+ console.log();
304
+ console.log(chalk.yellow('Permission denied.'));
305
+ if (isWindows) {
306
+ console.log(chalk.gray('Try running the terminal as Administrator.'));
307
+ } else {
308
+ console.log(chalk.gray('Check file permissions for the project directory.'));
309
+ }
310
+ }
311
+
196
312
  resolvePromise(1);
197
313
  });
198
314
  });
@@ -6,6 +6,7 @@ import { listBackups, restoreIrisApp, deleteBackup } from '../../utils/iris/back
6
6
  import { linkVueProject } from '../../utils/iris/linker.js';
7
7
  import { showPermissionError } from '../../utils/permissionError.js';
8
8
  import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
9
+ import { acquireLock, releaseLock, LockTypes } from '../../utils/iris/lock.js';
9
10
 
10
11
  /**
11
12
  * iris-app-recover command - Restore a deleted Iris app from backup.
@@ -135,15 +136,43 @@ export const irisRecover = async (options = {}) => {
135
136
  return;
136
137
  }
137
138
 
138
- // Restore files
139
- console.log();
140
- console.log(chalk.blue('Restoring files...'));
141
-
142
- const restoreResult = await restoreIrisApp(backupPath, {
143
- restoreLocal: true,
144
- restoreLink: true
139
+ // Acquire recover lock to prevent concurrent recoveries
140
+ const lockBasePath = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
141
+ const lockResult = acquireLock(LockTypes.RECOVER, {
142
+ context: slug,
143
+ operation: `recovering ${slug}`,
144
+ basePath: lockBasePath
145
145
  });
146
146
 
147
+ // Track if lock was acquired (permission errors are non-fatal)
148
+ let lockAcquired = lockResult.acquired;
149
+ if (!lockResult.acquired) {
150
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
151
+ console.log(chalk.yellow('Warning: Could not create recover lock (permission issue). Proceeding without lock.'));
152
+ lockAcquired = false;
153
+ } else {
154
+ console.log(chalk.red('Cannot recover app:'));
155
+ console.log(chalk.yellow(lockResult.error));
156
+ return;
157
+ }
158
+ }
159
+
160
+ let restoreResult;
161
+ try {
162
+ // Restore files
163
+ console.log();
164
+ console.log(chalk.blue('Restoring files...'));
165
+
166
+ restoreResult = await restoreIrisApp(backupPath, {
167
+ restoreLocal: true,
168
+ restoreLink: true
169
+ });
170
+ } finally {
171
+ if (lockAcquired) {
172
+ releaseLock(LockTypes.RECOVER, { context: slug, basePath: lockBasePath });
173
+ }
174
+ }
175
+
147
176
  if (!restoreResult.success) {
148
177
  if (restoreResult.isPermissionError) {
149
178
  const targetDir = path.join(process.cwd(), EXPORT_ROOT, IRIS_APPS_DIR);
@@ -153,6 +182,17 @@ export const irisRecover = async (options = {}) => {
153
182
  backupPath,
154
183
  slug
155
184
  });
185
+ } else if (restoreResult.isFileLocked) {
186
+ console.log(chalk.red('Cannot restore - files are in use'));
187
+ console.log(chalk.yellow(restoreResult.error));
188
+ console.log();
189
+ console.log(chalk.gray('Close any programs that may be using these files and try again.'));
190
+ } else if (restoreResult.isCorrupted) {
191
+ console.log(chalk.red('Backup appears to be corrupted'));
192
+ console.log(chalk.yellow(restoreResult.error));
193
+ console.log();
194
+ console.log(chalk.gray('The backup file may have been damaged. You may need to delete it.'));
195
+ console.log(chalk.gray(`Backup path: ${backupPath}`));
156
196
  } else {
157
197
  console.log(chalk.red(`Failed to restore: ${restoreResult.error}`));
158
198
  }
@@ -12,6 +12,7 @@ import {
12
12
  EXPORT_ROOT,
13
13
  TYPE_DIR_MAP,
14
14
  IRIS_APPS_DIR,
15
+ ALLOWED_SRC_DIRS,
15
16
  } from "../vars/global.js";
16
17
  import { getFileTag, setFileTag } from "../utils/filetag.js";
17
18
  import { sha256 } from "../utils/hash.js";
@@ -870,16 +871,19 @@ export const runPublish = async (options = {}) => {
870
871
  progress.completeStep('load', `✓ Loaded ${cachedFiles.length} entries (${loadTime}ms load, ${mapTime}ms map)`);
871
872
  }
872
873
 
873
- // Step 3: Scan local workspace (excluding Assets and iris-apps folders)
874
+ // Step 3: Scan local workspace (only whitelisted code entity directories)
874
875
  if (progress) progress.startStep('scan');
875
876
 
876
877
  const walkStart = Date.now();
877
- const localPaths = await walkFiles(EXPORT_ROOT, {
878
- ignore: [
879
- path.join(EXPORT_ROOT, 'Assets'),
880
- path.join(EXPORT_ROOT, IRIS_APPS_DIR)
881
- ]
882
- });
878
+ // Only scan whitelisted directories for code entities (exclude Assets and iris-apps, handled separately)
879
+ const codeEntityDirs = ALLOWED_SRC_DIRS.filter(dir => dir !== 'Assets' && dir !== IRIS_APPS_DIR);
880
+ const localPathArrays = await Promise.all(
881
+ codeEntityDirs.map(dir => {
882
+ const dirPath = path.join(EXPORT_ROOT, dir);
883
+ return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
884
+ })
885
+ );
886
+ const localPaths = localPathArrays.flat();
883
887
  const walkTime = Date.now() - walkStart;
884
888
 
885
889
  const tagStart = Date.now();
package/bin/magentrix.js CHANGED
@@ -20,6 +20,7 @@ import { configWizard } from '../actions/config.js';
20
20
  import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
21
21
  import Config from '../utils/config.js';
22
22
  import { registerWorkspace, getRegisteredWorkspaces } from '../utils/workspaces.js';
23
+ import { ensureVSCodeFileAssociation } from '../utils/preferences.js';
23
24
 
24
25
  const config = new Config();
25
26
 
@@ -102,6 +103,13 @@ function ensureWorkspaceRegistered() {
102
103
 
103
104
  async function preMiddleware() {
104
105
  ensureWorkspaceRegistered();
106
+
107
+ // Ensure .vscode folder exists in project root (not in src/) for Magentrix projects
108
+ const magentrixDir = join(CWD, '.magentrix');
109
+ if (existsSync(magentrixDir)) {
110
+ await ensureVSCodeFileAssociation(CWD);
111
+ }
112
+
105
113
  await recacheFileIdIndex(EXPORT_ROOT);
106
114
  await cacheDir(EXPORT_ROOT);
107
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
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
@@ -4,7 +4,7 @@ import Config from './config.js';
4
4
  import { findFileByTag, getFileTag, isPathLinkedToTagByLastKnownPath, setFileTag } from './filetag.js';
5
5
  import { compressString } from './compress.js';
6
6
  import { sha256 } from './hash.js';
7
- import { EXPORT_ROOT } from '../vars/global.js';
7
+ import { EXPORT_ROOT, ALLOWED_SRC_DIRS, IRIS_APPS_DIR } from '../vars/global.js';
8
8
 
9
9
  const config = new Config();
10
10
 
@@ -19,8 +19,15 @@ export const cacheDir = async (dir) => {
19
19
 
20
20
  const absDir = path.resolve(dir);
21
21
 
22
- // Walk files but exclude Assets folder (tracked separately in base.json)
23
- const files = await walkFiles(absDir, { ignore: [path.join(absDir, 'Assets')] });
22
+ // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
23
+ const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
24
+ const fileArrays = await Promise.all(
25
+ codeEntityDirs.map(d => {
26
+ const dirPath = path.join(absDir, d);
27
+ return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
28
+ })
29
+ );
30
+ const files = fileArrays.flat();
24
31
 
25
32
  const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
26
33
 
@@ -108,10 +115,17 @@ export const cacheDir = async (dir) => {
108
115
  * @returns {Promise<void>}
109
116
  */
110
117
  export const recacheFileIdIndex = async (dir) => {
111
- // Exclude Assets folder - they don't use file tags, tracked in base.json instead
112
118
  const absDir = path.resolve(dir);
113
- const ignorePath = path.join(absDir, 'Assets');
114
- const files = await walkFiles(absDir, { ignore: [ignorePath] });
119
+
120
+ // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
121
+ const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
122
+ const fileArrays = await Promise.all(
123
+ codeEntityDirs.map(d => {
124
+ const dirPath = path.join(absDir, d);
125
+ return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
126
+ })
127
+ );
128
+ const files = fileArrays.flat();
115
129
  if (!files || files?.length < 1) return;
116
130
 
117
131
  // Process files in parallel batches of 50 for speed