@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.
@@ -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;