@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,330 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import Config from '../../utils/config.js';
|
|
6
|
+
import { runPublish } from '../publish.js';
|
|
7
|
+
import { isAutopublishRunning } from '../../utils/autopublishLock.js';
|
|
8
|
+
import {
|
|
9
|
+
buildVueProject,
|
|
10
|
+
stageToCliProject,
|
|
11
|
+
findDistDirectory,
|
|
12
|
+
formatBuildError,
|
|
13
|
+
formatValidationError
|
|
14
|
+
} from '../../utils/iris/builder.js';
|
|
15
|
+
import { validateIrisBuild } from '../../utils/iris/validator.js';
|
|
16
|
+
import {
|
|
17
|
+
readVueConfig,
|
|
18
|
+
formatMissingConfigError,
|
|
19
|
+
formatConfigErrors
|
|
20
|
+
} from '../../utils/iris/config-reader.js';
|
|
21
|
+
import {
|
|
22
|
+
getLinkedProjectsWithStatus,
|
|
23
|
+
linkVueProject,
|
|
24
|
+
findLinkedProjectByPath,
|
|
25
|
+
buildProjectChoices
|
|
26
|
+
} from '../../utils/iris/linker.js';
|
|
27
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
|
|
28
|
+
|
|
29
|
+
const config = new Config();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if the current directory appears to be a Magentrix workspace.
|
|
33
|
+
* @returns {boolean} - True if in a Magentrix workspace
|
|
34
|
+
*/
|
|
35
|
+
function isMagentrixWorkspace() {
|
|
36
|
+
const magentrixFolder = join(process.cwd(), '.magentrix');
|
|
37
|
+
const srcFolder = join(process.cwd(), 'src');
|
|
38
|
+
|
|
39
|
+
// Check for .magentrix folder
|
|
40
|
+
if (!existsSync(magentrixFolder)) return false;
|
|
41
|
+
|
|
42
|
+
// Check for src folder (typical workspace structure)
|
|
43
|
+
if (!existsSync(srcFolder)) return false;
|
|
44
|
+
|
|
45
|
+
// Check if credentials are configured (instanceUrl or apiKey in global config)
|
|
46
|
+
const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
|
|
47
|
+
const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
|
|
48
|
+
|
|
49
|
+
return !!(instanceUrl || apiKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* vue-build-stage command - Build a Vue project and stage to CLI workspace.
|
|
54
|
+
*
|
|
55
|
+
* Options:
|
|
56
|
+
* --path <dir> Specify Vue project path directly
|
|
57
|
+
* --skip-build Use existing dist/ without rebuilding
|
|
58
|
+
*/
|
|
59
|
+
export const vueBuildStage = async (options = {}) => {
|
|
60
|
+
process.stdout.write('\x1Bc'); // Clear console
|
|
61
|
+
|
|
62
|
+
const { path: pathOption, skipBuild } = options;
|
|
63
|
+
|
|
64
|
+
// Warn if not in a Magentrix workspace
|
|
65
|
+
if (!isMagentrixWorkspace()) {
|
|
66
|
+
console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
|
|
67
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
68
|
+
console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
|
|
69
|
+
console.log(chalk.white('It stages Vue.js build files to ') + chalk.cyan('src/iris-apps/<slug>/'));
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.white('Expected workspace indicators:'));
|
|
72
|
+
console.log(chalk.gray(' • .magentrix/ folder (config and cache)'));
|
|
73
|
+
console.log(chalk.gray(' • src/ folder (code files and assets)'));
|
|
74
|
+
console.log(chalk.gray(' • Magentrix credentials configured'));
|
|
75
|
+
console.log();
|
|
76
|
+
console.log(chalk.white('Current directory: ') + chalk.gray(process.cwd()));
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
const shouldContinue = await confirm({
|
|
80
|
+
message: 'Do you want to continue anyway?',
|
|
81
|
+
default: false
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!shouldContinue) {
|
|
85
|
+
console.log(chalk.gray('\nCancelled. Run this command from your Magentrix workspace.'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Determine which project to build
|
|
93
|
+
let projectPath = pathOption;
|
|
94
|
+
let vueConfig = null;
|
|
95
|
+
|
|
96
|
+
if (projectPath) {
|
|
97
|
+
// Path provided via option
|
|
98
|
+
projectPath = resolve(projectPath);
|
|
99
|
+
|
|
100
|
+
if (!existsSync(projectPath)) {
|
|
101
|
+
console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
vueConfig = readVueConfig(projectPath);
|
|
106
|
+
} else {
|
|
107
|
+
// Prompt user to select a project
|
|
108
|
+
const result = await selectProject();
|
|
109
|
+
if (!result) return; // User cancelled
|
|
110
|
+
|
|
111
|
+
projectPath = result.path;
|
|
112
|
+
vueConfig = result.config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate Vue config
|
|
116
|
+
if (!vueConfig.found) {
|
|
117
|
+
console.log(chalk.red(formatMissingConfigError(projectPath)));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (vueConfig.errors.length > 0) {
|
|
122
|
+
console.log(chalk.red(formatConfigErrors(vueConfig)));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { slug, appName } = vueConfig;
|
|
127
|
+
|
|
128
|
+
console.log(chalk.blue('\nVue Build & Stage'));
|
|
129
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
130
|
+
console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
|
|
131
|
+
console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
|
|
132
|
+
console.log();
|
|
133
|
+
|
|
134
|
+
// Ensure project is linked
|
|
135
|
+
const linked = findLinkedProjectByPath(projectPath);
|
|
136
|
+
if (!linked) {
|
|
137
|
+
const shouldLink = await confirm({
|
|
138
|
+
message: 'This project is not linked. Link it now?',
|
|
139
|
+
default: true
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (shouldLink) {
|
|
143
|
+
const linkResult = linkVueProject(projectPath);
|
|
144
|
+
if (!linkResult.success) {
|
|
145
|
+
console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk.green(`\u2713 Project linked`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let distPath;
|
|
153
|
+
|
|
154
|
+
if (skipBuild) {
|
|
155
|
+
// Use existing dist
|
|
156
|
+
distPath = findDistDirectory(projectPath);
|
|
157
|
+
|
|
158
|
+
if (!distPath) {
|
|
159
|
+
console.log(chalk.red('No existing dist/ directory found.'));
|
|
160
|
+
console.log(chalk.gray('Run without --skip-build to build the project.'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(chalk.yellow(`Using existing dist: ${distPath}`));
|
|
165
|
+
|
|
166
|
+
// Validate the existing build
|
|
167
|
+
const validation = validateIrisBuild(distPath);
|
|
168
|
+
if (!validation.valid) {
|
|
169
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(chalk.green('\u2713 Existing build is valid'));
|
|
174
|
+
} else {
|
|
175
|
+
// Build the project
|
|
176
|
+
console.log(chalk.blue('Building project...'));
|
|
177
|
+
console.log();
|
|
178
|
+
|
|
179
|
+
const buildResult = await buildVueProject(projectPath, { silent: false });
|
|
180
|
+
|
|
181
|
+
if (!buildResult.success) {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
distPath = buildResult.distPath;
|
|
188
|
+
console.log();
|
|
189
|
+
console.log(chalk.green(`\u2713 Build completed successfully`));
|
|
190
|
+
console.log(chalk.gray(` Output: ${distPath}`));
|
|
191
|
+
|
|
192
|
+
// Validate build output
|
|
193
|
+
const validation = validateIrisBuild(distPath);
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(chalk.red(formatValidationError(distPath, validation.errors)));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(chalk.green('\u2713 Build output validated'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Stage to CLI project
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.blue('Staging to CLI workspace...'));
|
|
206
|
+
|
|
207
|
+
const stageResult = stageToCliProject(distPath, slug);
|
|
208
|
+
|
|
209
|
+
if (!stageResult.success) {
|
|
210
|
+
console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
|
|
215
|
+
|
|
216
|
+
// Summary
|
|
217
|
+
console.log();
|
|
218
|
+
console.log(chalk.green('─'.repeat(48)));
|
|
219
|
+
console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.gray(`Staged to: ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
// Check if autopublish is running
|
|
225
|
+
if (isAutopublishRunning()) {
|
|
226
|
+
console.log(chalk.cyan('✓ Autopublish is running - changes will be deployed automatically'));
|
|
227
|
+
} else {
|
|
228
|
+
// Prompt to run publish now
|
|
229
|
+
const shouldPublish = await confirm({
|
|
230
|
+
message: 'Do you want to publish to Magentrix now?',
|
|
231
|
+
default: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (shouldPublish) {
|
|
235
|
+
console.log();
|
|
236
|
+
console.log(chalk.blue('Running publish...'));
|
|
237
|
+
console.log();
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await runPublish();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.log(chalk.red(`\nPublish failed: ${error.message}`));
|
|
243
|
+
console.log(chalk.gray('You can run it manually later with:'), chalk.yellow('magentrix publish'));
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
console.log();
|
|
247
|
+
console.log(chalk.cyan('Next steps:'));
|
|
248
|
+
console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
|
|
249
|
+
console.log(chalk.white(` • Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Prompt user to select a project.
|
|
256
|
+
*
|
|
257
|
+
* @returns {Promise<{path: string, config: object} | null>}
|
|
258
|
+
*/
|
|
259
|
+
async function selectProject() {
|
|
260
|
+
const projectsWithStatus = getLinkedProjectsWithStatus();
|
|
261
|
+
|
|
262
|
+
// Check if CWD is a Vue project (and not already linked)
|
|
263
|
+
const cwdConfig = readVueConfig(process.cwd());
|
|
264
|
+
const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
|
|
265
|
+
const cwdAlreadyLinked = projectsWithStatus.some(p =>
|
|
266
|
+
resolve(p.path) === resolve(process.cwd())
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Build choices using the helper
|
|
270
|
+
const choices = buildProjectChoices({
|
|
271
|
+
includeManual: true,
|
|
272
|
+
includeCancel: true,
|
|
273
|
+
showInvalid: true
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// If CWD is a valid Vue project and not linked, add it at the top
|
|
277
|
+
if (inVueProject && !cwdAlreadyLinked) {
|
|
278
|
+
choices.unshift({
|
|
279
|
+
name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
|
|
280
|
+
value: { type: 'cwd', path: process.cwd() }
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check if we have any projects to show
|
|
285
|
+
const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
|
|
286
|
+
|
|
287
|
+
if (!hasProjects) {
|
|
288
|
+
console.log(chalk.yellow('No linked Vue projects found.'));
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(chalk.gray('To get started:'));
|
|
291
|
+
console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-link')}`));
|
|
292
|
+
console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-build-stage --path /path/to/vue-project')}`));
|
|
293
|
+
console.log();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const choice = await select({
|
|
297
|
+
message: 'Which project do you want to build?',
|
|
298
|
+
choices
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (choice.type === 'cancel') {
|
|
302
|
+
console.log(chalk.gray('Cancelled.'));
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (choice.type === 'manual') {
|
|
307
|
+
const manualPath = await input({
|
|
308
|
+
message: 'Enter the path to your Vue project:',
|
|
309
|
+
validate: (value) => {
|
|
310
|
+
if (!value.trim()) {
|
|
311
|
+
return 'Path is required';
|
|
312
|
+
}
|
|
313
|
+
const resolved = resolve(value);
|
|
314
|
+
if (!existsSync(resolved)) {
|
|
315
|
+
return `Path does not exist: ${resolved}`;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const config = readVueConfig(resolve(manualPath));
|
|
322
|
+
return { path: resolve(manualPath), config };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Linked project or CWD
|
|
326
|
+
const config = readVueConfig(choice.path);
|
|
327
|
+
return { path: choice.path, config };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export default vueBuildStage;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { select, input, confirm } from '@inquirer/prompts';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import Config from '../../utils/config.js';
|
|
5
|
+
import { ensureValidCredentials } from '../../utils/cli/helpers/ensureCredentials.js';
|
|
6
|
+
import { backupIrisApp } from '../../utils/iris/backup.js';
|
|
7
|
+
import { getLinkedProjects, unlinkVueProject } from '../../utils/iris/linker.js';
|
|
8
|
+
import { deleteIrisAppFromServer, deleteLocalIrisAppFiles } from '../../utils/iris/deleteHelper.js';
|
|
9
|
+
import { showPermissionError } from '../../utils/permissionError.js';
|
|
10
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
|
|
11
|
+
|
|
12
|
+
const config = new Config();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* iris-delete command - Delete a published Iris app with backup and recovery.
|
|
16
|
+
*/
|
|
17
|
+
export const irisDelete = async () => {
|
|
18
|
+
process.stdout.write('\x1Bc'); // Clear console
|
|
19
|
+
|
|
20
|
+
console.log(chalk.red.bold('\n⚠ Delete Iris App'));
|
|
21
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
22
|
+
console.log();
|
|
23
|
+
|
|
24
|
+
// Get list of published apps from base.json
|
|
25
|
+
const cachedResults = config.read(null, { filename: "base.json" });
|
|
26
|
+
const cachedIrisApps = Object.values(cachedResults || {})
|
|
27
|
+
.filter(entry => entry.type === 'IrisApp' || entry.Type === 'IrisApp')
|
|
28
|
+
.map(entry => {
|
|
29
|
+
const slug = entry.folderName || (entry.recordId && entry.recordId.startsWith('iris-app:')
|
|
30
|
+
? entry.recordId.replace('iris-app:', '')
|
|
31
|
+
: null);
|
|
32
|
+
return {
|
|
33
|
+
slug,
|
|
34
|
+
appName: entry.appName || slug,
|
|
35
|
+
folderName: entry.folderName || slug
|
|
36
|
+
};
|
|
37
|
+
})
|
|
38
|
+
.filter(app => app.slug);
|
|
39
|
+
|
|
40
|
+
if (cachedIrisApps.length === 0) {
|
|
41
|
+
console.log(chalk.yellow('No published Iris apps found.'));
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(chalk.gray('Published apps appear here after running:'));
|
|
44
|
+
console.log(chalk.white(` 1. ${chalk.cyan('magentrix vue-build-stage')}`));
|
|
45
|
+
console.log(chalk.white(` 2. ${chalk.cyan('magentrix publish')}`));
|
|
46
|
+
console.log();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build choices
|
|
51
|
+
const choices = cachedIrisApps.map(app => ({
|
|
52
|
+
name: `${app.appName} (${app.slug})`,
|
|
53
|
+
value: app
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
choices.push({
|
|
57
|
+
name: 'Cancel',
|
|
58
|
+
value: null
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Select app to delete
|
|
62
|
+
const selectedApp = await select({
|
|
63
|
+
message: 'Which Iris app do you want to delete?',
|
|
64
|
+
choices
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!selectedApp) {
|
|
68
|
+
console.log(chalk.gray('Cancelled.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { slug, appName } = selectedApp;
|
|
73
|
+
|
|
74
|
+
// Show destructive warning
|
|
75
|
+
console.log();
|
|
76
|
+
console.log(chalk.bgRed.bold.white(' ⚠ DESTRUCTIVE OPERATION '));
|
|
77
|
+
console.log(chalk.red('─'.repeat(48)));
|
|
78
|
+
console.log(chalk.white('This will permanently delete:'));
|
|
79
|
+
console.log(chalk.red(` • App from server: ${chalk.cyan(appName)} (${slug})`));
|
|
80
|
+
console.log(chalk.red(` • Local files: ${chalk.gray(`src/iris-apps/${slug}/`)}`));
|
|
81
|
+
console.log(chalk.red(` • Navigation menu entry on Magentrix`));
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(chalk.yellow('A recovery backup will be created before deletion.'));
|
|
84
|
+
console.log(chalk.gray('You can restore using: ') + chalk.cyan('magentrix iris-recover'));
|
|
85
|
+
console.log(chalk.red('─'.repeat(48)));
|
|
86
|
+
console.log();
|
|
87
|
+
|
|
88
|
+
// Confirm deletion by typing app name
|
|
89
|
+
const confirmation = await input({
|
|
90
|
+
message: `Type the app slug "${slug}" to confirm deletion:`,
|
|
91
|
+
validate: (value) => {
|
|
92
|
+
if (value === slug) return true;
|
|
93
|
+
return `Please type exactly: ${slug}`;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (confirmation !== slug) {
|
|
98
|
+
console.log(chalk.gray('Cancelled.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if app is linked
|
|
103
|
+
const linkedProjects = getLinkedProjects();
|
|
104
|
+
const linkedProject = linkedProjects.find(p => p.slug === slug);
|
|
105
|
+
|
|
106
|
+
let shouldUnlink = false;
|
|
107
|
+
if (linkedProject) {
|
|
108
|
+
console.log();
|
|
109
|
+
shouldUnlink = await confirm({
|
|
110
|
+
message: `This app is linked to a Vue project at ${linkedProject.path}. Unlink it?`,
|
|
111
|
+
default: false
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Create backup
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(chalk.blue('Creating recovery backup...'));
|
|
118
|
+
|
|
119
|
+
const appPath = path.join(EXPORT_ROOT, IRIS_APPS_DIR, slug);
|
|
120
|
+
const backupResult = await backupIrisApp(appPath, {
|
|
121
|
+
slug,
|
|
122
|
+
appName,
|
|
123
|
+
linkedProject: linkedProject || null
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!backupResult.success) {
|
|
127
|
+
console.log(chalk.red(`Failed to create backup: ${backupResult.error}`));
|
|
128
|
+
console.log(chalk.yellow('Deletion cancelled for safety.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(chalk.green(`✓ Backup created: ${backupResult.backupPath}`));
|
|
133
|
+
|
|
134
|
+
// Delete from server
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(chalk.blue('Deleting from Magentrix server...'));
|
|
137
|
+
|
|
138
|
+
const { instanceUrl, token } = await ensureValidCredentials();
|
|
139
|
+
const deleteResult = await deleteIrisAppFromServer(instanceUrl, token.value, slug, {
|
|
140
|
+
updateCache: true // Automatically updates base.json
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!deleteResult.success && deleteResult.error !== 'App not found on server (already deleted)') {
|
|
144
|
+
console.log(chalk.red(`Failed to delete from server: ${deleteResult.error}`));
|
|
145
|
+
console.log(chalk.yellow('Backup preserved. Use ') + chalk.cyan('magentrix iris-recover') + chalk.yellow(' to restore.'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (deleteResult.error === 'App not found on server (already deleted)') {
|
|
150
|
+
console.log(chalk.yellow('⚠ App not found on server (already deleted)'));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(chalk.green('✓ Deleted from server'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (deleteResult.cleanedFromCache) {
|
|
156
|
+
console.log(chalk.green('✓ Cache updated'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Delete local files
|
|
160
|
+
console.log(chalk.blue('Deleting local files...'));
|
|
161
|
+
|
|
162
|
+
const localDeleteResult = deleteLocalIrisAppFiles(appPath);
|
|
163
|
+
|
|
164
|
+
if (localDeleteResult.success) {
|
|
165
|
+
if (localDeleteResult.existed) {
|
|
166
|
+
console.log(chalk.green('✓ Local files deleted'));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(chalk.gray(' (No local files found)'));
|
|
169
|
+
}
|
|
170
|
+
} else if (localDeleteResult.isPermissionError) {
|
|
171
|
+
showPermissionError({
|
|
172
|
+
operation: 'delete',
|
|
173
|
+
targetPath: appPath
|
|
174
|
+
});
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(chalk.gray('Note: The app was deleted from the server and cache.'));
|
|
177
|
+
console.log(chalk.gray('Only local file cleanup failed.'));
|
|
178
|
+
console.log();
|
|
179
|
+
} else {
|
|
180
|
+
console.log(chalk.yellow(`⚠ Failed to delete local files: ${localDeleteResult.error}`));
|
|
181
|
+
console.log(chalk.gray(`Path: ${appPath}`));
|
|
182
|
+
console.log(chalk.white('You may need to delete manually.'));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Unlink Vue project if requested
|
|
186
|
+
if (shouldUnlink && linkedProject) {
|
|
187
|
+
console.log(chalk.blue('Unlinking Vue project...'));
|
|
188
|
+
unlinkVueProject(linkedProject.path);
|
|
189
|
+
console.log(chalk.green('✓ Vue project unlinked'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Summary
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(chalk.green('─'.repeat(48)));
|
|
195
|
+
if (localDeleteResult.success) {
|
|
196
|
+
console.log(chalk.green.bold('✓ Iris App Deleted Successfully!'));
|
|
197
|
+
} else {
|
|
198
|
+
console.log(chalk.yellow.bold('⚠ Iris App Partially Deleted'));
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(chalk.green('✓ Deleted from server'));
|
|
201
|
+
console.log(chalk.green('✓ Cache updated'));
|
|
202
|
+
console.log(chalk.yellow('⚠ Local files require manual deletion'));
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.cyan('Recovery:'));
|
|
206
|
+
console.log(chalk.white(` Backup saved to: ${chalk.gray(backupResult.backupPath)}`));
|
|
207
|
+
console.log(chalk.white(` To restore, run: ${chalk.cyan('magentrix iris-recover')}`));
|
|
208
|
+
console.log();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default irisDelete;
|