@magentrix-corp/magentrix-cli 1.3.15 → 1.3.17

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.
Files changed (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -45
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. package/vars/global.js +50 -50
@@ -1,375 +1,375 @@
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-app-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 Vue project',
81
- value: 'link',
82
- description: chalk.dim('→ 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: chalk.dim('→ Show all linked Vue projects with status')
91
- });
92
- choices.push({
93
- name: 'Unlink a project',
94
- value: 'unlink',
95
- description: chalk.dim('→ 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: chalk.dim('→ 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 (${cwdPath})`,
156
- value: cwdPath,
157
- description: chalk.dim(`→ App: "${cwdConfig.appName}" (${cwdConfig.slug})`)
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 (${cwdPath})`,
164
- value: '__disabled__',
165
- disabled: `Config error: ${errorMsg}`
166
- });
167
- } else {
168
- // No config.ts found
169
- choices.push({
170
- name: `Current directory (${cwdPath})`,
171
- value: '__disabled__',
172
- disabled: 'No config.ts found'
173
- });
174
- }
175
-
176
- choices.push({
177
- name: 'Enter path manually',
178
- value: '__manual__',
179
- description: chalk.dim('→ Specify the full path to a Vue project')
180
- });
181
-
182
- choices.push({
183
- name: 'Cancel',
184
- value: '__cancel__'
185
- });
186
-
187
- const choice = await select({
188
- message: 'Which Vue project do you want to link?',
189
- choices
190
- });
191
-
192
- if (choice === '__cancel__') {
193
- console.log(chalk.gray('Cancelled.'));
194
- return;
195
- }
196
-
197
- if (choice === '__manual__') {
198
- projectPath = await input({
199
- message: 'Enter the path to your Vue project:',
200
- validate: (value) => {
201
- if (!value.trim()) {
202
- return 'Path is required';
203
- }
204
- const resolved = resolve(value);
205
- if (!existsSync(resolved)) {
206
- return `Path does not exist: ${resolved}`;
207
- }
208
- return true;
209
- }
210
- });
211
- } else {
212
- projectPath = choice;
213
- }
214
- }
215
-
216
- await linkProjectDirect(projectPath);
217
- }
218
-
219
- /**
220
- * Link a project directly by path.
221
- */
222
- async function linkProjectDirect(projectPath) {
223
- // Resolve the path
224
- projectPath = resolve(projectPath);
225
-
226
- // Validate path exists
227
- if (!existsSync(projectPath)) {
228
- console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
229
- return;
230
- }
231
-
232
- // Check Vue config
233
- const vueConfig = readVueConfig(projectPath);
234
- if (!vueConfig.found) {
235
- console.log(chalk.red(formatMissingConfigError(projectPath)));
236
- return;
237
- }
238
-
239
- if (vueConfig.errors.length > 0) {
240
- console.log(chalk.red(formatConfigErrors(vueConfig)));
241
- return;
242
- }
243
-
244
- // Link the project
245
- console.log(chalk.blue(`\nLinking Vue project...`));
246
- console.log(chalk.gray(` Path: ${projectPath}`));
247
- console.log(chalk.gray(` Slug: ${vueConfig.slug}`));
248
- console.log(chalk.gray(` Name: ${vueConfig.appName}`));
249
- if (vueConfig.siteUrl) {
250
- console.log(chalk.gray(` Site: ${vueConfig.siteUrl}`));
251
- }
252
- console.log();
253
-
254
- const result = linkVueProject(projectPath);
255
-
256
- if (!result.success) {
257
- console.log(chalk.red(`Failed to link project: ${result.error}`));
258
- return;
259
- }
260
-
261
- if (result.updated) {
262
- console.log(chalk.green(`\u2713 Project updated successfully!`));
263
- console.log(chalk.gray(` The linked project configuration has been updated.`));
264
- } else {
265
- console.log(chalk.green(`\u2713 Project linked successfully!`));
266
- console.log();
267
- console.log(chalk.cyan('Next steps:'));
268
- console.log(chalk.white(` 1. Build and stage: ${chalk.yellow('magentrix vue-run-build')}`));
269
- console.log(chalk.white(` 2. Publish to server: ${chalk.yellow('magentrix publish')}`));
270
- console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic publishing`));
271
- }
272
-
273
- console.log();
274
- console.log(chalk.gray('Note: Linked projects are stored globally and available across all Magentrix workspaces.'));
275
- }
276
-
277
- /**
278
- * Handle unlinking a Vue project.
279
- */
280
- async function handleUnlink(pathOption) {
281
- const linkedProjects = getLinkedProjectsWithStatus();
282
-
283
- if (linkedProjects.length === 0) {
284
- console.log(chalk.yellow('No Vue projects are currently linked.'));
285
- return;
286
- }
287
-
288
- let projectToUnlink = pathOption;
289
-
290
- // If no path provided, prompt user to select
291
- if (!projectToUnlink) {
292
- const choices = linkedProjects.map(p => {
293
- const validation = p.validation;
294
- let prefix = '';
295
- if (!validation.valid) {
296
- prefix = validation.exists ? '⚠ ' : '✗ ';
297
- }
298
- const displayName = validation.currentAppName || p.appName;
299
- const displaySlug = validation.currentSlug || p.slug;
300
-
301
- return {
302
- name: `${prefix}${displayName} (${displaySlug})`,
303
- value: p.slug,
304
- description: chalk.dim(`→ Path: ${p.path}`)
305
- };
306
- });
307
-
308
- choices.push({
309
- name: 'Cancel',
310
- value: '__cancel__'
311
- });
312
-
313
- projectToUnlink = await select({
314
- message: 'Which project do you want to unlink?',
315
- choices
316
- });
317
-
318
- if (projectToUnlink === '__cancel__') {
319
- console.log(chalk.gray('Cancelled.'));
320
- return;
321
- }
322
- }
323
-
324
- // Unlink the project
325
- const result = unlinkVueProject(projectToUnlink);
326
-
327
- if (!result.success) {
328
- console.log(chalk.red(`Failed to unlink: ${result.error}`));
329
- return;
330
- }
331
-
332
- console.log(chalk.green(`\u2713 Project '${result.project.appName}' (${result.project.slug}) has been unlinked.`));
333
- console.log(chalk.gray(` Path: ${result.project.path}`));
334
- console.log();
335
- console.log(chalk.gray('Note: This only removes the link from CLI tracking.'));
336
- console.log(chalk.gray('The Vue project and any deployed Iris app are unchanged.'));
337
- }
338
-
339
- /**
340
- * Handle cleanup of invalid projects.
341
- */
342
- async function handleCleanup() {
343
- const projectsWithStatus = getLinkedProjectsWithStatus();
344
- const invalidProjects = projectsWithStatus.filter(p => !p.validation.exists);
345
-
346
- if (invalidProjects.length === 0) {
347
- console.log(chalk.green('All linked projects are valid. No cleanup needed.'));
348
- return;
349
- }
350
-
351
- console.log(chalk.yellow(`Found ${invalidProjects.length} project(s) with invalid paths:\n`));
352
-
353
- for (const project of invalidProjects) {
354
- console.log(chalk.red(` ✗ ${project.appName} (${project.slug})`));
355
- console.log(chalk.gray(` Path: ${project.path}`));
356
- console.log(chalk.gray(` Error: ${project.validation.errors.join(', ')}`));
357
- console.log();
358
- }
359
-
360
- const shouldCleanup = await confirm({
361
- message: `Remove these ${invalidProjects.length} invalid project(s)?`,
362
- default: true
363
- });
364
-
365
- if (!shouldCleanup) {
366
- console.log(chalk.gray('Cleanup cancelled.'));
367
- return;
368
- }
369
-
370
- const result = cleanupInvalidProjects();
371
-
372
- console.log(chalk.green(`\u2713 Removed ${result.removed} invalid project(s).`));
373
- }
374
-
375
- export default irisLink;
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-app-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 Vue project',
81
+ value: 'link',
82
+ description: chalk.dim('→ 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: chalk.dim('→ Show all linked Vue projects with status')
91
+ });
92
+ choices.push({
93
+ name: 'Unlink a project',
94
+ value: 'unlink',
95
+ description: chalk.dim('→ 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: chalk.dim('→ 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 (${cwdPath})`,
156
+ value: cwdPath,
157
+ description: chalk.dim(`→ App: "${cwdConfig.appName}" (${cwdConfig.slug})`)
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 (${cwdPath})`,
164
+ value: '__disabled__',
165
+ disabled: `Config error: ${errorMsg}`
166
+ });
167
+ } else {
168
+ // No config.ts found
169
+ choices.push({
170
+ name: `Current directory (${cwdPath})`,
171
+ value: '__disabled__',
172
+ disabled: 'No config.ts found'
173
+ });
174
+ }
175
+
176
+ choices.push({
177
+ name: 'Enter path manually',
178
+ value: '__manual__',
179
+ description: chalk.dim('→ Specify the full path to a Vue project')
180
+ });
181
+
182
+ choices.push({
183
+ name: 'Cancel',
184
+ value: '__cancel__'
185
+ });
186
+
187
+ const choice = await select({
188
+ message: 'Which Vue project do you want to link?',
189
+ choices
190
+ });
191
+
192
+ if (choice === '__cancel__') {
193
+ console.log(chalk.gray('Cancelled.'));
194
+ return;
195
+ }
196
+
197
+ if (choice === '__manual__') {
198
+ projectPath = await input({
199
+ message: 'Enter the path to your Vue project:',
200
+ validate: (value) => {
201
+ if (!value.trim()) {
202
+ return 'Path is required';
203
+ }
204
+ const resolved = resolve(value);
205
+ if (!existsSync(resolved)) {
206
+ return `Path does not exist: ${resolved}`;
207
+ }
208
+ return true;
209
+ }
210
+ });
211
+ } else {
212
+ projectPath = choice;
213
+ }
214
+ }
215
+
216
+ await linkProjectDirect(projectPath);
217
+ }
218
+
219
+ /**
220
+ * Link a project directly by path.
221
+ */
222
+ async function linkProjectDirect(projectPath) {
223
+ // Resolve the path
224
+ projectPath = resolve(projectPath);
225
+
226
+ // Validate path exists
227
+ if (!existsSync(projectPath)) {
228
+ console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
229
+ return;
230
+ }
231
+
232
+ // Check Vue config
233
+ const vueConfig = readVueConfig(projectPath);
234
+ if (!vueConfig.found) {
235
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
236
+ return;
237
+ }
238
+
239
+ if (vueConfig.errors.length > 0) {
240
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
241
+ return;
242
+ }
243
+
244
+ // Link the project
245
+ console.log(chalk.blue(`\nLinking Vue project...`));
246
+ console.log(chalk.gray(` Path: ${projectPath}`));
247
+ console.log(chalk.gray(` Slug: ${vueConfig.slug}`));
248
+ console.log(chalk.gray(` Name: ${vueConfig.appName}`));
249
+ if (vueConfig.siteUrl) {
250
+ console.log(chalk.gray(` Site: ${vueConfig.siteUrl}`));
251
+ }
252
+ console.log();
253
+
254
+ const result = linkVueProject(projectPath);
255
+
256
+ if (!result.success) {
257
+ console.log(chalk.red(`Failed to link project: ${result.error}`));
258
+ return;
259
+ }
260
+
261
+ if (result.updated) {
262
+ console.log(chalk.green(`\u2713 Project updated successfully!`));
263
+ console.log(chalk.gray(` The linked project configuration has been updated.`));
264
+ } else {
265
+ console.log(chalk.green(`\u2713 Project linked successfully!`));
266
+ console.log();
267
+ console.log(chalk.cyan('Next steps:'));
268
+ console.log(chalk.white(` 1. Build and stage: ${chalk.yellow('magentrix vue-run-build')}`));
269
+ console.log(chalk.white(` 2. Publish to server: ${chalk.yellow('magentrix publish')}`));
270
+ console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic publishing`));
271
+ }
272
+
273
+ console.log();
274
+ console.log(chalk.gray('Note: Linked projects are stored globally and available across all Magentrix workspaces.'));
275
+ }
276
+
277
+ /**
278
+ * Handle unlinking a Vue project.
279
+ */
280
+ async function handleUnlink(pathOption) {
281
+ const linkedProjects = getLinkedProjectsWithStatus();
282
+
283
+ if (linkedProjects.length === 0) {
284
+ console.log(chalk.yellow('No Vue projects are currently linked.'));
285
+ return;
286
+ }
287
+
288
+ let projectToUnlink = pathOption;
289
+
290
+ // If no path provided, prompt user to select
291
+ if (!projectToUnlink) {
292
+ const choices = linkedProjects.map(p => {
293
+ const validation = p.validation;
294
+ let prefix = '';
295
+ if (!validation.valid) {
296
+ prefix = validation.exists ? '⚠ ' : '✗ ';
297
+ }
298
+ const displayName = validation.currentAppName || p.appName;
299
+ const displaySlug = validation.currentSlug || p.slug;
300
+
301
+ return {
302
+ name: `${prefix}${displayName} (${displaySlug})`,
303
+ value: p.slug,
304
+ description: chalk.dim(`→ Path: ${p.path}`)
305
+ };
306
+ });
307
+
308
+ choices.push({
309
+ name: 'Cancel',
310
+ value: '__cancel__'
311
+ });
312
+
313
+ projectToUnlink = await select({
314
+ message: 'Which project do you want to unlink?',
315
+ choices
316
+ });
317
+
318
+ if (projectToUnlink === '__cancel__') {
319
+ console.log(chalk.gray('Cancelled.'));
320
+ return;
321
+ }
322
+ }
323
+
324
+ // Unlink the project
325
+ const result = unlinkVueProject(projectToUnlink);
326
+
327
+ if (!result.success) {
328
+ console.log(chalk.red(`Failed to unlink: ${result.error}`));
329
+ return;
330
+ }
331
+
332
+ console.log(chalk.green(`\u2713 Project '${result.project.appName}' (${result.project.slug}) has been unlinked.`));
333
+ console.log(chalk.gray(` Path: ${result.project.path}`));
334
+ console.log();
335
+ console.log(chalk.gray('Note: This only removes the link from CLI tracking.'));
336
+ console.log(chalk.gray('The Vue project and any deployed Iris app are unchanged.'));
337
+ }
338
+
339
+ /**
340
+ * Handle cleanup of invalid projects.
341
+ */
342
+ async function handleCleanup() {
343
+ const projectsWithStatus = getLinkedProjectsWithStatus();
344
+ const invalidProjects = projectsWithStatus.filter(p => !p.validation.exists);
345
+
346
+ if (invalidProjects.length === 0) {
347
+ console.log(chalk.green('All linked projects are valid. No cleanup needed.'));
348
+ return;
349
+ }
350
+
351
+ console.log(chalk.yellow(`Found ${invalidProjects.length} project(s) with invalid paths:\n`));
352
+
353
+ for (const project of invalidProjects) {
354
+ console.log(chalk.red(` ✗ ${project.appName} (${project.slug})`));
355
+ console.log(chalk.gray(` Path: ${project.path}`));
356
+ console.log(chalk.gray(` Error: ${project.validation.errors.join(', ')}`));
357
+ console.log();
358
+ }
359
+
360
+ const shouldCleanup = await confirm({
361
+ message: `Remove these ${invalidProjects.length} invalid project(s)?`,
362
+ default: true
363
+ });
364
+
365
+ if (!shouldCleanup) {
366
+ console.log(chalk.gray('Cleanup cancelled.'));
367
+ return;
368
+ }
369
+
370
+ const result = cleanupInvalidProjects();
371
+
372
+ console.log(chalk.green(`\u2713 Removed ${result.removed} invalid project(s).`));
373
+ }
374
+
375
+ export default irisLink;