@magentrix-corp/magentrix-cli 1.3.9 → 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/README.md +42 -4
- 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 +210 -35
- 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
package/utils/iris/backup.js
CHANGED
|
@@ -2,9 +2,21 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { createIrisZip } from './zipper.js';
|
|
4
4
|
import { extractIrisZip } from './zipper.js';
|
|
5
|
+
import {
|
|
6
|
+
detectErrorType,
|
|
7
|
+
ErrorTypes,
|
|
8
|
+
formatPermissionError,
|
|
9
|
+
formatDiskFullError,
|
|
10
|
+
formatFileLockError
|
|
11
|
+
} from './errors.js';
|
|
5
12
|
|
|
6
13
|
const BACKUP_DIR = '.magentrix/iris-backups';
|
|
7
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Maximum backup age in days. Older backups will be cleaned up.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_BACKUP_AGE_DAYS = 30;
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* Create a backup of an Iris app before deletion.
|
|
10
22
|
*
|
|
@@ -16,20 +28,89 @@ const BACKUP_DIR = '.magentrix/iris-backups';
|
|
|
16
28
|
* @returns {Promise<{success: boolean, backupPath: string | null, error: string | null}>}
|
|
17
29
|
*/
|
|
18
30
|
export async function backupIrisApp(appPath, metadata) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const backupPath = path.join(process.cwd(), BACKUP_DIR, backupName);
|
|
31
|
+
const { slug } = metadata;
|
|
32
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
33
|
+
const backupName = `${slug}-${timestamp}`;
|
|
34
|
+
const backupPath = path.join(process.cwd(), BACKUP_DIR, backupName);
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
try {
|
|
37
|
+
// Ensure backup directory exists
|
|
26
38
|
fs.mkdirSync(backupPath, { recursive: true });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const errorType = detectErrorType(err);
|
|
41
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
backupPath: null,
|
|
45
|
+
error: formatPermissionError({
|
|
46
|
+
operation: 'create backup directory',
|
|
47
|
+
path: backupPath
|
|
48
|
+
}),
|
|
49
|
+
isPermissionError: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (errorType === ErrorTypes.DISK_FULL) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
backupPath: null,
|
|
56
|
+
error: formatDiskFullError({
|
|
57
|
+
operation: 'create backup',
|
|
58
|
+
path: backupPath
|
|
59
|
+
}),
|
|
60
|
+
isDiskFull: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
backupPath: null,
|
|
66
|
+
error: `Failed to create backup directory: ${err.message}`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
27
69
|
|
|
70
|
+
try {
|
|
28
71
|
// Create ZIP of app files
|
|
29
72
|
const zipBuffer = await createIrisZip(appPath, slug);
|
|
30
73
|
const zipPath = path.join(backupPath, `${slug}.zip`);
|
|
31
74
|
fs.writeFileSync(zipPath, zipBuffer);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// Clean up partial backup
|
|
77
|
+
try {
|
|
78
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore cleanup errors
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const errorType = detectErrorType(err);
|
|
84
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
backupPath: null,
|
|
88
|
+
error: formatPermissionError({
|
|
89
|
+
operation: 'write backup files',
|
|
90
|
+
path: backupPath
|
|
91
|
+
}),
|
|
92
|
+
isPermissionError: true
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (errorType === ErrorTypes.DISK_FULL) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
backupPath: null,
|
|
99
|
+
error: formatDiskFullError({
|
|
100
|
+
operation: 'create backup',
|
|
101
|
+
path: backupPath
|
|
102
|
+
}),
|
|
103
|
+
isDiskFull: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
backupPath: null,
|
|
109
|
+
error: `Failed to create backup ZIP: ${err.message}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
32
112
|
|
|
113
|
+
try {
|
|
33
114
|
// Save metadata
|
|
34
115
|
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
35
116
|
fs.writeFileSync(metadataPath, JSON.stringify({
|
|
@@ -38,19 +119,25 @@ export async function backupIrisApp(appPath, metadata) {
|
|
|
38
119
|
backupName,
|
|
39
120
|
originalPath: appPath
|
|
40
121
|
}, null, 2));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
backupPath,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Clean up partial backup
|
|
124
|
+
try {
|
|
125
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore cleanup errors
|
|
128
|
+
}
|
|
48
129
|
return {
|
|
49
130
|
success: false,
|
|
50
131
|
backupPath: null,
|
|
51
|
-
error:
|
|
132
|
+
error: `Failed to save backup metadata: ${err.message}`
|
|
52
133
|
};
|
|
53
134
|
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
backupPath,
|
|
139
|
+
error: null
|
|
140
|
+
};
|
|
54
141
|
}
|
|
55
142
|
|
|
56
143
|
/**
|
|
@@ -128,74 +215,197 @@ export async function restoreIrisApp(backupPath, options = {}) {
|
|
|
128
215
|
warnings: [],
|
|
129
216
|
error: null,
|
|
130
217
|
isPermissionError: false,
|
|
218
|
+
isFileLocked: false,
|
|
219
|
+
isCorrupted: false,
|
|
131
220
|
targetPath: null
|
|
132
221
|
};
|
|
133
222
|
|
|
223
|
+
// Read metadata
|
|
224
|
+
const metadataPath = path.join(backupPath, 'metadata.json');
|
|
225
|
+
if (!fs.existsSync(metadataPath)) {
|
|
226
|
+
result.error = 'Backup metadata not found. The backup may be incomplete or corrupted.';
|
|
227
|
+
result.isCorrupted = true;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let metadata;
|
|
134
232
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
233
|
+
const metadataContent = fs.readFileSync(metadataPath, 'utf-8');
|
|
234
|
+
metadata = JSON.parse(metadataContent);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (err instanceof SyntaxError) {
|
|
237
|
+
result.error = 'Backup metadata is corrupted (invalid JSON). The backup may be damaged.';
|
|
238
|
+
result.isCorrupted = true;
|
|
239
|
+
} else {
|
|
240
|
+
result.error = `Failed to read backup metadata: ${err.message}`;
|
|
140
241
|
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
141
244
|
|
|
142
|
-
|
|
143
|
-
const { slug, linkedProject } = metadata;
|
|
245
|
+
const { slug, linkedProject } = metadata;
|
|
144
246
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
247
|
+
if (!slug) {
|
|
248
|
+
result.error = 'Backup metadata is missing required "slug" field. The backup may be corrupted.';
|
|
249
|
+
result.isCorrupted = true;
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Restore local files
|
|
254
|
+
if (restoreLocal) {
|
|
255
|
+
const zipPath = path.join(backupPath, `${slug}.zip`);
|
|
256
|
+
if (!fs.existsSync(zipPath)) {
|
|
257
|
+
result.error = 'Backup ZIP file not found. The backup may be incomplete.';
|
|
258
|
+
result.isCorrupted = true;
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let zipBuffer;
|
|
263
|
+
try {
|
|
264
|
+
zipBuffer = fs.readFileSync(zipPath);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const errorType = detectErrorType(err);
|
|
267
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
268
|
+
result.error = formatPermissionError({
|
|
269
|
+
operation: 'read backup file',
|
|
270
|
+
path: zipPath
|
|
271
|
+
});
|
|
272
|
+
result.isPermissionError = true;
|
|
273
|
+
} else if (errorType === ErrorTypes.FILE_LOCKED) {
|
|
274
|
+
result.error = formatFileLockError({ path: zipPath });
|
|
275
|
+
result.isFileLocked = true;
|
|
276
|
+
} else {
|
|
277
|
+
result.error = `Failed to read backup file: ${err.message}`;
|
|
151
278
|
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
152
281
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
result.targetPath = appTargetDir;
|
|
282
|
+
const targetDir = path.join(process.cwd(), 'src', 'iris-apps');
|
|
283
|
+
const appTargetDir = path.join(targetDir, slug);
|
|
284
|
+
result.targetPath = appTargetDir;
|
|
157
285
|
|
|
158
|
-
|
|
286
|
+
// Ensure target directory exists
|
|
287
|
+
try {
|
|
159
288
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
const errorType = detectErrorType(err);
|
|
291
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
292
|
+
result.error = formatPermissionError({
|
|
293
|
+
operation: 'create restore directory',
|
|
294
|
+
path: targetDir
|
|
295
|
+
});
|
|
296
|
+
result.isPermissionError = true;
|
|
297
|
+
} else {
|
|
298
|
+
result.error = `Failed to create restore directory: ${err.message}`;
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
160
302
|
|
|
161
|
-
|
|
303
|
+
// Extract backup
|
|
304
|
+
try {
|
|
162
305
|
await extractIrisZip(zipBuffer, targetDir);
|
|
163
306
|
result.restoredFiles = true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
result.
|
|
172
|
-
|
|
173
|
-
);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const errorType = detectErrorType(err);
|
|
309
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
310
|
+
result.error = formatPermissionError({
|
|
311
|
+
operation: 'extract backup files',
|
|
312
|
+
path: appTargetDir
|
|
313
|
+
});
|
|
314
|
+
result.isPermissionError = true;
|
|
315
|
+
} else if (errorType === ErrorTypes.FILE_LOCKED) {
|
|
316
|
+
result.error = formatFileLockError({ path: appTargetDir });
|
|
317
|
+
result.isFileLocked = true;
|
|
318
|
+
} else if (errorType === ErrorTypes.DISK_FULL) {
|
|
319
|
+
result.error = formatDiskFullError({
|
|
320
|
+
operation: 'restore backup',
|
|
321
|
+
path: appTargetDir
|
|
322
|
+
});
|
|
323
|
+
} else if (err.message?.includes('invalid') || err.message?.includes('corrupt')) {
|
|
324
|
+
result.error = 'Backup ZIP file appears to be corrupted. The backup cannot be restored.';
|
|
325
|
+
result.isCorrupted = true;
|
|
174
326
|
} else {
|
|
175
|
-
result.
|
|
327
|
+
result.error = `Failed to extract backup: ${err.message}`;
|
|
176
328
|
}
|
|
329
|
+
return result;
|
|
177
330
|
}
|
|
331
|
+
}
|
|
178
332
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
333
|
+
// Restore linked project
|
|
334
|
+
if (restoreLink && linkedProject) {
|
|
335
|
+
// Check if the linked project path still exists
|
|
336
|
+
if (!fs.existsSync(linkedProject.path)) {
|
|
337
|
+
result.linkedProjectPathExists = false;
|
|
338
|
+
result.warnings.push(
|
|
339
|
+
`Linked Vue project no longer exists at: ${linkedProject.path}`
|
|
340
|
+
);
|
|
341
|
+
} else {
|
|
342
|
+
result.restoredLink = true;
|
|
343
|
+
}
|
|
185
344
|
}
|
|
345
|
+
|
|
346
|
+
result.success = true;
|
|
347
|
+
return result;
|
|
186
348
|
}
|
|
187
349
|
|
|
188
350
|
/**
|
|
189
351
|
* Delete a backup folder.
|
|
190
352
|
*
|
|
191
353
|
* @param {string} backupPath - Path to the backup folder
|
|
354
|
+
* @returns {{success: boolean, error: string | null}}
|
|
192
355
|
*/
|
|
193
356
|
export function deleteBackup(backupPath) {
|
|
194
357
|
try {
|
|
195
358
|
if (fs.existsSync(backupPath)) {
|
|
196
359
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
197
360
|
}
|
|
198
|
-
|
|
199
|
-
|
|
361
|
+
return { success: true, error: null };
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const errorType = detectErrorType(err);
|
|
364
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: formatPermissionError({
|
|
368
|
+
operation: 'delete backup',
|
|
369
|
+
path: backupPath
|
|
370
|
+
})
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (errorType === ErrorTypes.FILE_LOCKED) {
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: formatFileLockError({ path: backupPath })
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
error: `Failed to delete backup: ${err.message}`
|
|
382
|
+
};
|
|
200
383
|
}
|
|
201
384
|
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clean up old backups (older than MAX_BACKUP_AGE_DAYS).
|
|
388
|
+
*
|
|
389
|
+
* @returns {{cleaned: number, errors: number}}
|
|
390
|
+
*/
|
|
391
|
+
export function cleanupOldBackups() {
|
|
392
|
+
const result = { cleaned: 0, errors: 0 };
|
|
393
|
+
const backups = listBackups();
|
|
394
|
+
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const maxAge = MAX_BACKUP_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
397
|
+
|
|
398
|
+
for (const backup of backups) {
|
|
399
|
+
const age = now - new Date(backup.deletedAt).getTime();
|
|
400
|
+
if (age > maxAge) {
|
|
401
|
+
const deleteResult = deleteBackup(backup.backupPath);
|
|
402
|
+
if (deleteResult.success) {
|
|
403
|
+
result.cleaned++;
|
|
404
|
+
} else {
|
|
405
|
+
result.errors++;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return result;
|
|
411
|
+
}
|