@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.
- package/README.md +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +258 -15
- package/actions/pull.js +520 -327
- package/actions/setup.js +62 -15
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/cli/helpers/ensureApiKey.js +28 -22
- package/utils/cli/helpers/ensureInstanceUrl.js +35 -27
- package/utils/cli/writeRecords.js +13 -2
- package/utils/config.js +76 -0
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/auth.js +45 -6
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +14 -2
- package/vars/global.js +1 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
linkVueProject,
|
|
7
|
+
unlinkVueProject,
|
|
8
|
+
getLinkedProjectsWithStatus,
|
|
9
|
+
formatLinkedProjects,
|
|
10
|
+
cleanupInvalidProjects
|
|
11
|
+
} from '../../utils/iris/linker.js';
|
|
12
|
+
import { formatMissingConfigError, formatConfigErrors, readVueConfig } from '../../utils/iris/config-reader.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* iris-link command - Link a Vue project to the CLI.
|
|
16
|
+
*
|
|
17
|
+
* Options:
|
|
18
|
+
* --path <dir> Specify Vue project path directly
|
|
19
|
+
* --unlink Remove a linked project
|
|
20
|
+
* --list Show all linked projects
|
|
21
|
+
* --cleanup Remove invalid (non-existent) linked projects
|
|
22
|
+
*/
|
|
23
|
+
export const irisLink = async (options = {}) => {
|
|
24
|
+
process.stdout.write('\x1Bc'); // Clear console
|
|
25
|
+
|
|
26
|
+
const { path: pathOption, unlink, list, cleanup } = options;
|
|
27
|
+
|
|
28
|
+
// Handle --list option
|
|
29
|
+
if (list) {
|
|
30
|
+
const projects = getLinkedProjectsWithStatus();
|
|
31
|
+
console.log(formatLinkedProjects(projects));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle --cleanup option
|
|
36
|
+
if (cleanup) {
|
|
37
|
+
await handleCleanup();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle --unlink option
|
|
42
|
+
if (unlink) {
|
|
43
|
+
await handleUnlink(pathOption);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If path provided directly, link it
|
|
48
|
+
if (pathOption) {
|
|
49
|
+
await linkProjectDirect(pathOption);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Interactive mode - show main menu
|
|
54
|
+
await showMainMenu();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Show the main menu for managing linked projects.
|
|
59
|
+
*/
|
|
60
|
+
async function showMainMenu() {
|
|
61
|
+
const projects = getLinkedProjectsWithStatus();
|
|
62
|
+
const validCount = projects.filter(p => p.validation.valid).length;
|
|
63
|
+
const invalidCount = projects.filter(p => !p.validation.valid).length;
|
|
64
|
+
|
|
65
|
+
console.log(chalk.blue.bold('Iris Vue Project Manager'));
|
|
66
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
67
|
+
|
|
68
|
+
if (projects.length > 0) {
|
|
69
|
+
console.log(chalk.white(`Linked projects: ${validCount} valid`));
|
|
70
|
+
if (invalidCount > 0) {
|
|
71
|
+
console.log(chalk.yellow(` ${invalidCount} with issues`));
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log(chalk.gray('No Vue projects linked yet.'));
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
const choices = [
|
|
79
|
+
{
|
|
80
|
+
name: 'Link a new Vue project',
|
|
81
|
+
value: 'link',
|
|
82
|
+
description: 'Add a Vue project to the CLI'
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (projects.length > 0) {
|
|
87
|
+
choices.push({
|
|
88
|
+
name: 'View linked projects',
|
|
89
|
+
value: 'list',
|
|
90
|
+
description: 'Show all linked Vue projects with status'
|
|
91
|
+
});
|
|
92
|
+
choices.push({
|
|
93
|
+
name: 'Unlink a project',
|
|
94
|
+
value: 'unlink',
|
|
95
|
+
description: 'Remove a Vue project from the CLI'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (invalidCount > 0) {
|
|
100
|
+
choices.push({
|
|
101
|
+
name: `Clean up invalid projects (${invalidCount})`,
|
|
102
|
+
value: 'cleanup',
|
|
103
|
+
description: 'Remove projects with missing paths'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
choices.push({
|
|
108
|
+
name: 'Exit',
|
|
109
|
+
value: 'exit'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const action = await select({
|
|
113
|
+
message: 'What would you like to do?',
|
|
114
|
+
choices
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
switch (action) {
|
|
118
|
+
case 'link':
|
|
119
|
+
await handleLink();
|
|
120
|
+
break;
|
|
121
|
+
case 'list':
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(formatLinkedProjects(projects));
|
|
124
|
+
break;
|
|
125
|
+
case 'unlink':
|
|
126
|
+
await handleUnlink();
|
|
127
|
+
break;
|
|
128
|
+
case 'cleanup':
|
|
129
|
+
await handleCleanup();
|
|
130
|
+
break;
|
|
131
|
+
case 'exit':
|
|
132
|
+
console.log(chalk.gray('Goodbye!'));
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle linking a new Vue project.
|
|
139
|
+
*/
|
|
140
|
+
async function handleLink(pathOption) {
|
|
141
|
+
let projectPath = pathOption;
|
|
142
|
+
|
|
143
|
+
// If no path provided, prompt for one
|
|
144
|
+
if (!projectPath) {
|
|
145
|
+
// Check if we're in a Vue project directory
|
|
146
|
+
const cwdConfig = readVueConfig(process.cwd());
|
|
147
|
+
const cwdPath = process.cwd();
|
|
148
|
+
|
|
149
|
+
const choices = [];
|
|
150
|
+
|
|
151
|
+
// Always show current directory option, but disable if not a valid Vue project
|
|
152
|
+
if (cwdConfig.found && cwdConfig.errors.length === 0) {
|
|
153
|
+
// Valid Vue project
|
|
154
|
+
choices.push({
|
|
155
|
+
name: `Current directory - ${cwdConfig.appName} (${cwdConfig.slug})`,
|
|
156
|
+
value: cwdPath,
|
|
157
|
+
description: cwdPath
|
|
158
|
+
});
|
|
159
|
+
} else if (cwdConfig.found && cwdConfig.errors.length > 0) {
|
|
160
|
+
// Has config.ts but with errors
|
|
161
|
+
const errorMsg = cwdConfig.errors[0] || 'Invalid config';
|
|
162
|
+
choices.push({
|
|
163
|
+
name: `Current directory`,
|
|
164
|
+
value: '__disabled__',
|
|
165
|
+
disabled: `Config error: ${errorMsg}`,
|
|
166
|
+
description: cwdPath
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
// No config.ts found
|
|
170
|
+
choices.push({
|
|
171
|
+
name: `Current directory`,
|
|
172
|
+
value: '__disabled__',
|
|
173
|
+
disabled: 'No config.ts found',
|
|
174
|
+
description: cwdPath
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
choices.push({
|
|
179
|
+
name: 'Enter path manually',
|
|
180
|
+
value: '__manual__',
|
|
181
|
+
description: 'Specify the full path to a Vue project'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
choices.push({
|
|
185
|
+
name: 'Cancel',
|
|
186
|
+
value: '__cancel__'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const choice = await select({
|
|
190
|
+
message: 'Which Vue project do you want to link?',
|
|
191
|
+
choices
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (choice === '__cancel__') {
|
|
195
|
+
console.log(chalk.gray('Cancelled.'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (choice === '__manual__') {
|
|
200
|
+
projectPath = await input({
|
|
201
|
+
message: 'Enter the path to your Vue project:',
|
|
202
|
+
validate: (value) => {
|
|
203
|
+
if (!value.trim()) {
|
|
204
|
+
return 'Path is required';
|
|
205
|
+
}
|
|
206
|
+
const resolved = resolve(value);
|
|
207
|
+
if (!existsSync(resolved)) {
|
|
208
|
+
return `Path does not exist: ${resolved}`;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
projectPath = choice;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await linkProjectDirect(projectPath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Link a project directly by path.
|
|
223
|
+
*/
|
|
224
|
+
async function linkProjectDirect(projectPath) {
|
|
225
|
+
// Resolve the path
|
|
226
|
+
projectPath = resolve(projectPath);
|
|
227
|
+
|
|
228
|
+
// Validate path exists
|
|
229
|
+
if (!existsSync(projectPath)) {
|
|
230
|
+
console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check Vue config
|
|
235
|
+
const vueConfig = readVueConfig(projectPath);
|
|
236
|
+
if (!vueConfig.found) {
|
|
237
|
+
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (vueConfig.errors.length > 0) {
|
|
242
|
+
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Link the project
|
|
247
|
+
console.log(chalk.blue(`\nLinking Vue project...`));
|
|
248
|
+
console.log(chalk.gray(` Path: ${projectPath}`));
|
|
249
|
+
console.log(chalk.gray(` Slug: ${vueConfig.slug}`));
|
|
250
|
+
console.log(chalk.gray(` Name: ${vueConfig.appName}`));
|
|
251
|
+
if (vueConfig.siteUrl) {
|
|
252
|
+
console.log(chalk.gray(` Site: ${vueConfig.siteUrl}`));
|
|
253
|
+
}
|
|
254
|
+
console.log();
|
|
255
|
+
|
|
256
|
+
const result = linkVueProject(projectPath);
|
|
257
|
+
|
|
258
|
+
if (!result.success) {
|
|
259
|
+
console.log(chalk.red(`Failed to link project: ${result.error}`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.updated) {
|
|
264
|
+
console.log(chalk.green(`\u2713 Project updated successfully!`));
|
|
265
|
+
console.log(chalk.gray(` The linked project configuration has been updated.`));
|
|
266
|
+
} else {
|
|
267
|
+
console.log(chalk.green(`\u2713 Project linked successfully!`));
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(chalk.cyan('Next steps:'));
|
|
270
|
+
console.log(chalk.white(` 1. Build and stage: ${chalk.yellow('magentrix vue-build-stage')}`));
|
|
271
|
+
console.log(chalk.white(` 2. Publish to server: ${chalk.yellow('magentrix publish')}`));
|
|
272
|
+
console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic publishing`));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(chalk.gray('Note: Linked projects are stored globally and available across all Magentrix workspaces.'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle unlinking a Vue project.
|
|
281
|
+
*/
|
|
282
|
+
async function handleUnlink(pathOption) {
|
|
283
|
+
const linkedProjects = getLinkedProjectsWithStatus();
|
|
284
|
+
|
|
285
|
+
if (linkedProjects.length === 0) {
|
|
286
|
+
console.log(chalk.yellow('No Vue projects are currently linked.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let projectToUnlink = pathOption;
|
|
291
|
+
|
|
292
|
+
// If no path provided, prompt user to select
|
|
293
|
+
if (!projectToUnlink) {
|
|
294
|
+
const choices = linkedProjects.map(p => {
|
|
295
|
+
const validation = p.validation;
|
|
296
|
+
let prefix = '';
|
|
297
|
+
if (!validation.valid) {
|
|
298
|
+
prefix = validation.exists ? '⚠ ' : '✗ ';
|
|
299
|
+
}
|
|
300
|
+
const displayName = validation.currentAppName || p.appName;
|
|
301
|
+
const displaySlug = validation.currentSlug || p.slug;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
name: `${prefix}${displayName} (${displaySlug})`,
|
|
305
|
+
value: p.slug,
|
|
306
|
+
description: p.path
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
choices.push({
|
|
311
|
+
name: 'Cancel',
|
|
312
|
+
value: '__cancel__'
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
projectToUnlink = await select({
|
|
316
|
+
message: 'Which project do you want to unlink?',
|
|
317
|
+
choices
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (projectToUnlink === '__cancel__') {
|
|
321
|
+
console.log(chalk.gray('Cancelled.'));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Unlink the project
|
|
327
|
+
const result = unlinkVueProject(projectToUnlink);
|
|
328
|
+
|
|
329
|
+
if (!result.success) {
|
|
330
|
+
console.log(chalk.red(`Failed to unlink: ${result.error}`));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log(chalk.green(`\u2713 Project '${result.project.appName}' (${result.project.slug}) has been unlinked.`));
|
|
335
|
+
console.log(chalk.gray(` Path: ${result.project.path}`));
|
|
336
|
+
console.log();
|
|
337
|
+
console.log(chalk.gray('Note: This only removes the link from CLI tracking.'));
|
|
338
|
+
console.log(chalk.gray('The Vue project and any deployed Iris app are unchanged.'));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Handle cleanup of invalid projects.
|
|
343
|
+
*/
|
|
344
|
+
async function handleCleanup() {
|
|
345
|
+
const projectsWithStatus = getLinkedProjectsWithStatus();
|
|
346
|
+
const invalidProjects = projectsWithStatus.filter(p => !p.validation.exists);
|
|
347
|
+
|
|
348
|
+
if (invalidProjects.length === 0) {
|
|
349
|
+
console.log(chalk.green('All linked projects are valid. No cleanup needed.'));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(chalk.yellow(`Found ${invalidProjects.length} project(s) with invalid paths:\n`));
|
|
354
|
+
|
|
355
|
+
for (const project of invalidProjects) {
|
|
356
|
+
console.log(chalk.red(` ✗ ${project.appName} (${project.slug})`));
|
|
357
|
+
console.log(chalk.gray(` Path: ${project.path}`));
|
|
358
|
+
console.log(chalk.gray(` Error: ${project.validation.errors.join(', ')}`));
|
|
359
|
+
console.log();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const shouldCleanup = await confirm({
|
|
363
|
+
message: `Remove these ${invalidProjects.length} invalid project(s)?`,
|
|
364
|
+
default: true
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (!shouldCleanup) {
|
|
368
|
+
console.log(chalk.gray('Cleanup cancelled.'));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const result = cleanupInvalidProjects();
|
|
373
|
+
|
|
374
|
+
console.log(chalk.green(`\u2713 Removed ${result.removed} invalid project(s).`));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export default irisLink;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { listBackups, restoreIrisApp, deleteBackup } from '../../utils/iris/backup.js';
|
|
6
|
+
import { linkVueProject } from '../../utils/iris/linker.js';
|
|
7
|
+
import { showPermissionError } from '../../utils/permissionError.js';
|
|
8
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* iris-recover command - Restore a deleted Iris app from backup.
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --list List all available backups
|
|
15
|
+
*/
|
|
16
|
+
export const irisRecover = async (options = {}) => {
|
|
17
|
+
process.stdout.write('\x1Bc'); // Clear console
|
|
18
|
+
|
|
19
|
+
const { list } = options;
|
|
20
|
+
|
|
21
|
+
console.log(chalk.blue.bold('\n♻ Recover Iris App'));
|
|
22
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
// Get available backups
|
|
26
|
+
const backups = listBackups();
|
|
27
|
+
|
|
28
|
+
if (backups.length === 0) {
|
|
29
|
+
console.log(chalk.yellow('No recovery backups found.'));
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(chalk.gray('Backups are created automatically when you delete an Iris app.'));
|
|
32
|
+
console.log(chalk.white(`Use: ${chalk.cyan('magentrix iris-delete')}`));
|
|
33
|
+
console.log();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If --list flag, just show and exit
|
|
38
|
+
if (list) {
|
|
39
|
+
console.log(chalk.white('Available Recovery Backups:'));
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
backups.forEach((backup, i) => {
|
|
43
|
+
const date = new Date(backup.deletedAt);
|
|
44
|
+
console.log(chalk.white(`${i + 1}. ${chalk.cyan(backup.appName)} (${backup.slug})`));
|
|
45
|
+
console.log(chalk.gray(` Deleted: ${date.toLocaleString()}`));
|
|
46
|
+
if (backup.linkedProject) {
|
|
47
|
+
console.log(chalk.gray(` Linked: ${backup.linkedProject.path}`));
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.gray(` Backup: ${backup.backupPath}`));
|
|
50
|
+
console.log();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(chalk.white(`To recover, run: ${chalk.cyan('magentrix iris-recover')}`));
|
|
54
|
+
console.log();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build choices for selection
|
|
59
|
+
const choices = backups.map((backup) => {
|
|
60
|
+
const date = new Date(backup.deletedAt);
|
|
61
|
+
const timeAgo = getTimeAgo(date);
|
|
62
|
+
return {
|
|
63
|
+
name: `${backup.appName} (${backup.slug}) - Deleted ${timeAgo}`,
|
|
64
|
+
value: backup
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
choices.push({
|
|
69
|
+
name: 'Cancel',
|
|
70
|
+
value: null
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Select backup to restore
|
|
74
|
+
const selectedBackup = await select({
|
|
75
|
+
message: 'Which backup do you want to restore?',
|
|
76
|
+
choices
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!selectedBackup) {
|
|
80
|
+
console.log(chalk.gray('Cancelled.'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { slug, appName, linkedProject, backupPath } = selectedBackup;
|
|
85
|
+
|
|
86
|
+
// Show recovery info
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(chalk.white('Recovery Details:'));
|
|
89
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
90
|
+
console.log(chalk.white(` App: ${chalk.cyan(appName)} (${slug})`));
|
|
91
|
+
console.log(chalk.white(` Backup: ${chalk.gray(backupPath)}`));
|
|
92
|
+
|
|
93
|
+
if (linkedProject) {
|
|
94
|
+
const pathExists = existsSync(linkedProject.path);
|
|
95
|
+
if (pathExists) {
|
|
96
|
+
console.log(chalk.green(` ✓ Linked project: ${linkedProject.path}`));
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.yellow(` ⚠ Linked project path no longer exists:`));
|
|
99
|
+
console.log(chalk.gray(` ${linkedProject.path}`));
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log(chalk.gray(' (No linked Vue project)'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
// Check for warnings
|
|
108
|
+
const warnings = [];
|
|
109
|
+
if (linkedProject && !existsSync(linkedProject.path)) {
|
|
110
|
+
warnings.push('The linked Vue project path no longer exists. Only local files will be restored.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if app already exists locally
|
|
114
|
+
const appPath = path.join(EXPORT_ROOT, IRIS_APPS_DIR, slug);
|
|
115
|
+
if (existsSync(appPath)) {
|
|
116
|
+
console.log(chalk.yellow(`⚠ Warning: App folder already exists at ${appPath}`));
|
|
117
|
+
console.log(chalk.yellow(' Recovery will overwrite existing files.'));
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (warnings.length > 0) {
|
|
122
|
+
console.log(chalk.yellow('⚠ Warnings:'));
|
|
123
|
+
warnings.forEach(w => console.log(chalk.yellow(` • ${w}`)));
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Confirm recovery
|
|
128
|
+
const shouldRecover = await confirm({
|
|
129
|
+
message: 'Do you want to restore this app?',
|
|
130
|
+
default: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!shouldRecover) {
|
|
134
|
+
console.log(chalk.gray('Cancelled.'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
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
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!restoreResult.success) {
|
|
148
|
+
if (restoreResult.isPermissionError) {
|
|
149
|
+
const targetDir = path.join(process.cwd(), EXPORT_ROOT, IRIS_APPS_DIR);
|
|
150
|
+
showPermissionError({
|
|
151
|
+
operation: 'restore',
|
|
152
|
+
targetPath: targetDir,
|
|
153
|
+
backupPath,
|
|
154
|
+
slug
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
console.log(chalk.red(`Failed to restore: ${restoreResult.error}`));
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.green(`✓ Restored files to ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
|
|
163
|
+
|
|
164
|
+
// Re-link Vue project if needed
|
|
165
|
+
if (linkedProject && restoreResult.linkedProjectPathExists) {
|
|
166
|
+
console.log(chalk.blue('Re-linking Vue project...'));
|
|
167
|
+
|
|
168
|
+
const linkResult = linkVueProject(linkedProject.path);
|
|
169
|
+
if (linkResult.success) {
|
|
170
|
+
console.log(chalk.green('✓ Vue project re-linked'));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(chalk.yellow(`⚠ Could not re-link Vue project: ${linkResult.error}`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Show warnings
|
|
177
|
+
if (restoreResult.warnings.length > 0) {
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(chalk.yellow('Warnings:'));
|
|
180
|
+
restoreResult.warnings.forEach(w => console.log(chalk.yellow(` • ${w}`)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Summary
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(chalk.green('─'.repeat(48)));
|
|
186
|
+
console.log(chalk.green.bold('✓ Recovery Complete!'));
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(chalk.cyan('Next steps:'));
|
|
189
|
+
console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to sync the app back to the server`));
|
|
190
|
+
console.log();
|
|
191
|
+
|
|
192
|
+
// Ask if they want to delete the backup
|
|
193
|
+
const deleteBackupConfirm = await confirm({
|
|
194
|
+
message: 'Delete the recovery backup now?',
|
|
195
|
+
default: false
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (deleteBackupConfirm) {
|
|
199
|
+
deleteBackup(backupPath);
|
|
200
|
+
console.log(chalk.green('✓ Recovery backup deleted'));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(chalk.gray(`Backup preserved at: ${backupPath}`));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get human-readable time ago string.
|
|
210
|
+
* @param {Date} date - Date to compare
|
|
211
|
+
* @returns {string} - Time ago string
|
|
212
|
+
*/
|
|
213
|
+
function getTimeAgo(date) {
|
|
214
|
+
const now = new Date();
|
|
215
|
+
const diffMs = now - date;
|
|
216
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
217
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
218
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
219
|
+
|
|
220
|
+
if (diffMins < 1) return 'just now';
|
|
221
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
|
222
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
223
|
+
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
224
|
+
|
|
225
|
+
return date.toLocaleDateString();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default irisRecover;
|