@magentrix-corp/magentrix-cli 1.3.10 → 1.3.11
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/actions/iris/buildStage.js +93 -5
- package/actions/iris/delete.js +46 -1
- package/actions/iris/dev.js +129 -13
- package/actions/iris/recover.js +47 -7
- package/package.json +1 -1
- package/utils/iris/backup.js +262 -52
- package/utils/iris/builder.js +334 -112
- package/utils/iris/config-reader.js +206 -31
- package/utils/iris/deleteHelper.js +55 -7
- package/utils/iris/errors.js +537 -0
- package/utils/iris/linker.js +118 -13
- package/utils/iris/lock.js +360 -0
- package/utils/iris/validation.js +360 -0
|
@@ -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
|
-
|
|
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
|
});
|
package/actions/iris/delete.js
CHANGED
|
@@ -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;
|
package/actions/iris/dev.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
});
|
package/actions/iris/recover.js
CHANGED
|
@@ -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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
}
|