@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,874 +1,874 @@
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 { spawn } from 'node:child_process';
6
- import Config from '../../utils/config.js';
7
- import { runPublish } from '../publish.js';
8
- import { isAutopublishRunning } from '../../utils/autopublishLock.js';
9
- import {
10
- buildVueProject,
11
- stageToWorkspace,
12
- findDistDirectory,
13
- formatBuildError,
14
- formatValidationError
15
- } from '../../utils/iris/builder.js';
16
- import { validateIrisBuild } from '../../utils/iris/validator.js';
17
- import {
18
- readVueConfig,
19
- formatMissingConfigError,
20
- formatConfigErrors
21
- } from '../../utils/iris/config-reader.js';
22
- import {
23
- getLinkedProjectsWithStatus,
24
- linkVueProject,
25
- findLinkedProjectByPath,
26
- buildProjectChoices
27
- } from '../../utils/iris/linker.js';
28
- import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
29
- import { getValidWorkspaces } from '../../utils/workspaces.js';
30
- import { formatTimeoutError } from '../../utils/iris/errors.js';
31
-
32
- const config = new Config();
33
-
34
- /**
35
- * Timeout for status check operations (30 seconds).
36
- */
37
- const STATUS_CHECK_TIMEOUT = 30000;
38
-
39
- /**
40
- * Timeout for command execution (5 minutes).
41
- */
42
- const COMMAND_TIMEOUT = 5 * 60 * 1000;
43
-
44
- /**
45
- * Display warnings from build/stage operations.
46
- *
47
- * @param {string[]} warnings - Array of warning messages
48
- */
49
- function displayWarnings(warnings) {
50
- if (!warnings || warnings.length === 0) return;
51
-
52
- for (const warning of warnings) {
53
- console.log(chalk.yellow(`⚠ ${warning}`));
54
- }
55
- }
56
-
57
- /**
58
- * Check if the current directory is a Vue project (has config.ts).
59
- * @returns {boolean}
60
- */
61
- function isInVueProject() {
62
- const configLocations = [
63
- 'src/config.ts',
64
- 'config.ts',
65
- 'src/iris-config.ts',
66
- 'iris-config.ts'
67
- ];
68
- return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
69
- }
70
-
71
- /**
72
- * Check if the current directory appears to be a Magentrix workspace.
73
- * @returns {boolean} - True if in a Magentrix workspace
74
- */
75
- function isMagentrixWorkspace() {
76
- const magentrixFolder = join(process.cwd(), '.magentrix');
77
- const srcFolder = join(process.cwd(), 'src');
78
-
79
- // Check for .magentrix folder
80
- if (!existsSync(magentrixFolder)) return false;
81
-
82
- // Check for src folder (typical workspace structure)
83
- if (!existsSync(srcFolder)) return false;
84
-
85
- // Check if credentials are configured (instanceUrl or apiKey in global config)
86
- const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
87
- const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
88
-
89
- return !!(instanceUrl || apiKey);
90
- }
91
-
92
- /**
93
- * vue-run-build command - Build a Vue project and stage to CLI workspace.
94
- *
95
- * Two modes of operation:
96
- * 1. Run from Magentrix workspace: prompts for which Vue project to build
97
- * 2. Run from Vue project: prompts for which workspace to stage into
98
- *
99
- * Options:
100
- * --path <dir> Specify Vue project path directly
101
- * --skip-build Use existing dist/ without rebuilding
102
- * --workspace <dir> Specify Magentrix workspace path directly
103
- */
104
- export const vueBuildStage = async (options = {}) => {
105
- process.stdout.write('\x1Bc'); // Clear console
106
-
107
- const { path: pathOption, skipBuild, workspace: workspaceOption } = options;
108
-
109
- // Detect which mode we're in
110
- const inVueProject = isInVueProject();
111
- const inWorkspace = isMagentrixWorkspace();
112
-
113
- // If run from a Vue project, use reversed logic
114
- if (inVueProject && !inWorkspace) {
115
- await buildFromVueProject(options);
116
- return;
117
- }
118
-
119
- // Standard mode: run from workspace, select Vue project
120
- // Warn if not in a Magentrix workspace
121
- if (!inWorkspace) {
122
- console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
123
- console.log(chalk.gray('─'.repeat(48)));
124
- console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
125
- console.log(chalk.white('It stages Vue.js build files to ') + chalk.cyan('src/iris-apps/<slug>/'));
126
- console.log();
127
- console.log(chalk.white('Expected workspace indicators:'));
128
- console.log(chalk.gray(' • .magentrix/ folder (config and cache)'));
129
- console.log(chalk.gray(' • src/ folder (code files and assets)'));
130
- console.log(chalk.gray(' • Magentrix credentials configured'));
131
- console.log();
132
- console.log(chalk.white('Current directory: ') + chalk.gray(process.cwd()));
133
- console.log();
134
-
135
- const shouldContinue = await confirm({
136
- message: 'Do you want to continue anyway?',
137
- default: false
138
- });
139
-
140
- if (!shouldContinue) {
141
- console.log(chalk.gray('\nCancelled. Run this command from your Magentrix workspace.'));
142
- return;
143
- }
144
-
145
- console.log();
146
- }
147
-
148
- // Determine which project to build
149
- let projectPath = pathOption;
150
- let vueConfig = null;
151
-
152
- if (projectPath) {
153
- // Path provided via option
154
- projectPath = resolve(projectPath);
155
-
156
- if (!existsSync(projectPath)) {
157
- console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
158
- return;
159
- }
160
-
161
- vueConfig = readVueConfig(projectPath);
162
- } else {
163
- // Prompt user to select a project
164
- const result = await selectProject();
165
- if (!result) return; // User cancelled
166
-
167
- projectPath = result.path;
168
- vueConfig = result.config;
169
- }
170
-
171
- // Validate Vue config
172
- if (!vueConfig.found) {
173
- console.log(chalk.red(formatMissingConfigError(projectPath)));
174
- return;
175
- }
176
-
177
- if (vueConfig.errors.length > 0) {
178
- console.log(chalk.red(formatConfigErrors(vueConfig)));
179
- return;
180
- }
181
-
182
- const { slug, appName } = vueConfig;
183
-
184
- console.log(chalk.blue('\nVue Build & Stage'));
185
- console.log(chalk.gray('─'.repeat(48)));
186
- console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
187
- console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
188
- console.log();
189
-
190
- // Ensure project is linked
191
- const linked = findLinkedProjectByPath(projectPath);
192
- if (!linked) {
193
- const shouldLink = await confirm({
194
- message: 'This project is not linked. Link it now?',
195
- default: true
196
- });
197
-
198
- if (shouldLink) {
199
- const linkResult = linkVueProject(projectPath);
200
- if (!linkResult.success) {
201
- console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
202
- return;
203
- }
204
- console.log(chalk.green(`\u2713 Project linked`));
205
- }
206
- }
207
-
208
- let distPath;
209
-
210
- if (skipBuild) {
211
- // Use existing dist
212
- distPath = findDistDirectory(projectPath);
213
-
214
- if (!distPath) {
215
- console.log(chalk.red('No existing dist/ directory found.'));
216
- console.log(chalk.gray('Run without --skip-build to build the project.'));
217
- return;
218
- }
219
-
220
- console.log(chalk.yellow(`Using existing dist: ${distPath}`));
221
-
222
- // Validate the existing build
223
- const validation = validateIrisBuild(distPath);
224
- if (!validation.valid) {
225
- console.log(chalk.red(formatValidationError(distPath, validation.errors)));
226
- return;
227
- }
228
-
229
- console.log(chalk.green('\u2713 Existing build is valid'));
230
- } else {
231
- // Build the project
232
- console.log(chalk.blue('Building project...'));
233
- console.log();
234
-
235
- const buildResult = await buildVueProject(projectPath, { silent: false });
236
-
237
- if (!buildResult.success) {
238
- console.log();
239
- console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
240
- return;
241
- }
242
-
243
- distPath = buildResult.distPath;
244
- console.log();
245
- console.log(chalk.green(`\u2713 Build completed successfully`));
246
- console.log(chalk.gray(` Output: ${distPath}`));
247
-
248
- // Display any build warnings
249
- displayWarnings(buildResult.warnings);
250
-
251
- // Validate build output
252
- const validation = validateIrisBuild(distPath);
253
- if (!validation.valid) {
254
- console.log();
255
- console.log(chalk.red(formatValidationError(distPath, validation.errors)));
256
- return;
257
- }
258
-
259
- console.log(chalk.green('\u2713 Build output validated'));
260
- }
261
-
262
- // Stage to CLI project (current workspace)
263
- console.log();
264
- console.log(chalk.blue('Staging to CLI workspace...'));
265
-
266
- const stageResult = stageToWorkspace(distPath, slug, process.cwd());
267
-
268
- if (!stageResult.success) {
269
- console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
270
- return;
271
- }
272
-
273
- console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
274
-
275
- // Display any staging warnings
276
- displayWarnings(stageResult.warnings);
277
-
278
- // Summary
279
- console.log();
280
- console.log(chalk.green('─'.repeat(48)));
281
- console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
282
- console.log();
283
- console.log(chalk.gray(`Staged to: ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
284
- console.log();
285
-
286
- // Check if autopublish is running
287
- if (isAutopublishRunning()) {
288
- console.log(chalk.cyan('✓ Autopublish is running - changes will be deployed automatically'));
289
- } else {
290
- // Prompt to run publish now
291
- const shouldPublish = await confirm({
292
- message: 'Do you want to publish to Magentrix now?',
293
- default: true
294
- });
295
-
296
- if (shouldPublish) {
297
- console.log();
298
- console.log(chalk.blue('Running publish...'));
299
- console.log();
300
-
301
- try {
302
- await runPublish();
303
- } catch (error) {
304
- console.log(chalk.red(`\nPublish failed: ${error.message}`));
305
- console.log(chalk.gray('You can run it manually later with:'), chalk.yellow('magentrix publish'));
306
- }
307
- } else {
308
- console.log();
309
- console.log(chalk.cyan('Next steps:'));
310
- console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
311
- console.log(chalk.white(` • Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
312
- }
313
- }
314
- };
315
-
316
- /**
317
- * Build and stage when running from inside a Vue project.
318
- * Prompts user to select which workspace to stage into.
319
- */
320
- async function buildFromVueProject(options) {
321
- const { skipBuild, workspace: workspaceOption } = options;
322
-
323
- // Use current directory as Vue project
324
- const projectPath = process.cwd();
325
- const vueConfig = readVueConfig(projectPath);
326
-
327
- // Validate Vue config
328
- if (!vueConfig.found) {
329
- console.log(chalk.red(formatMissingConfigError(projectPath)));
330
- return;
331
- }
332
-
333
- if (vueConfig.errors.length > 0) {
334
- console.log(chalk.red(formatConfigErrors(vueConfig)));
335
- return;
336
- }
337
-
338
- const { slug, appName } = vueConfig;
339
-
340
- console.log(chalk.blue('\nVue Build & Stage'));
341
- console.log(chalk.gray('─'.repeat(48)));
342
- console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
343
- console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
344
- console.log();
345
-
346
- // Determine which workspace to stage into
347
- let workspacePath = workspaceOption;
348
-
349
- if (workspacePath) {
350
- workspacePath = resolve(workspacePath);
351
- if (!existsSync(workspacePath)) {
352
- console.log(chalk.red(`Error: Workspace path does not exist: ${workspacePath}`));
353
- return;
354
- }
355
- } else {
356
- // Prompt user to select a workspace
357
- const result = await selectWorkspace();
358
- if (!result) return; // User cancelled
359
- workspacePath = result;
360
- }
361
-
362
- // Ensure project is linked
363
- const linked = findLinkedProjectByPath(projectPath);
364
- if (!linked) {
365
- const shouldLink = await confirm({
366
- message: 'This project is not linked. Link it now?',
367
- default: true
368
- });
369
-
370
- if (shouldLink) {
371
- const linkResult = linkVueProject(projectPath);
372
- if (!linkResult.success) {
373
- console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
374
- return;
375
- }
376
- console.log(chalk.green(`\u2713 Project linked`));
377
- }
378
- }
379
-
380
- let distPath;
381
-
382
- if (skipBuild) {
383
- // Use existing dist
384
- distPath = findDistDirectory(projectPath);
385
-
386
- if (!distPath) {
387
- console.log(chalk.red('No existing dist/ directory found.'));
388
- console.log(chalk.gray('Run without --skip-build to build the project.'));
389
- return;
390
- }
391
-
392
- console.log(chalk.yellow(`Using existing dist: ${distPath}`));
393
-
394
- // Validate the existing build
395
- const validation = validateIrisBuild(distPath);
396
- if (!validation.valid) {
397
- console.log(chalk.red(formatValidationError(distPath, validation.errors)));
398
- return;
399
- }
400
-
401
- console.log(chalk.green('\u2713 Existing build is valid'));
402
- } else {
403
- // Build the project
404
- console.log(chalk.blue('Building project...'));
405
- console.log();
406
-
407
- const buildResult = await buildVueProject(projectPath, { silent: false });
408
-
409
- if (!buildResult.success) {
410
- console.log();
411
- console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
412
- return;
413
- }
414
-
415
- distPath = buildResult.distPath;
416
- console.log();
417
- console.log(chalk.green(`\u2713 Build completed successfully`));
418
- console.log(chalk.gray(` Output: ${distPath}`));
419
-
420
- // Display any build warnings
421
- displayWarnings(buildResult.warnings);
422
-
423
- // Validate build output
424
- const validation = validateIrisBuild(distPath);
425
- if (!validation.valid) {
426
- console.log();
427
- console.log(chalk.red(formatValidationError(distPath, validation.errors)));
428
- return;
429
- }
430
-
431
- console.log(chalk.green('\u2713 Build output validated'));
432
- }
433
-
434
- // Check if workspace might be out of sync and offer to pull first (BEFORE staging)
435
- // This ensures pull doesn't overwrite our staged files
436
- console.log();
437
- console.log(chalk.gray('Checking workspace sync status...'));
438
-
439
- // Check for a previous incomplete pull first — `magentrix status` only checks
440
- // code entities, so a partial pull that synced code but not assets would falsely
441
- // report "in sync". The marker file catches this case.
442
- const workspaceConfig = new Config({ projectDir: workspacePath });
443
- const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
444
-
445
- const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
446
- const needsPull = syncStatus.needsPull || !!hadIncompletePull;
447
-
448
- if (needsPull) {
449
- console.log();
450
- if (hadIncompletePull && !syncStatus.needsPull) {
451
- console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
452
- } else {
453
- console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
454
- }
455
-
456
- const shouldPull = await confirm({
457
- message: 'Would you like to pull latest changes first?',
458
- default: true
459
- });
460
-
461
- if (shouldPull) {
462
- console.log();
463
- console.log(chalk.blue('Running pull from workspace...'));
464
- console.log();
465
-
466
- // Mark pull as in-progress before starting
467
- workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
468
-
469
- const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
470
-
471
- if (pullSuccess) {
472
- // Pull completed successfully — clear the marker
473
- workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
474
- } else {
475
- // Pull failed or was cancelled — marker stays for next run
476
- console.log();
477
- console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
478
-
479
- const continueAnyway = await confirm({
480
- message: 'Do you still want to continue with staging and publishing?',
481
- default: false
482
- });
483
-
484
- if (!continueAnyway) {
485
- console.log();
486
- console.log(chalk.cyan('To continue manually:'));
487
- console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
488
- console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to resolve conflicts`));
489
- console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix vue-run-build --skip-build')} to stage`));
490
- console.log(chalk.white(` 4. Run ${chalk.yellow('magentrix publish')} to deploy`));
491
- return;
492
- }
493
- }
494
- console.log();
495
- } else {
496
- // User declined to pull - block publishing from out-of-sync workspace
497
- console.log();
498
- console.log(chalk.red('Publishing Iris apps from an out-of-sync workspace is not allowed.'));
499
- console.log(chalk.gray('This prevents conflicts and ensures your deployment is based on the latest server state.'));
500
- console.log();
501
- console.log(chalk.cyan('To continue:'));
502
- console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
503
- console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to sync with server`));
504
- console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix publish')} to deploy`));
505
- return;
506
- }
507
- } else if (syncStatus.checked) {
508
- console.log(chalk.green('✓ Workspace is in sync'));
509
- }
510
-
511
- // Stage to selected workspace (AFTER pull to avoid overwriting)
512
- console.log();
513
- console.log(chalk.blue(`Staging to workspace: ${workspacePath}`));
514
-
515
- const stageResult = stageToWorkspace(distPath, slug, workspacePath);
516
-
517
- if (!stageResult.success) {
518
- console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
519
- return;
520
- }
521
-
522
- console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
523
-
524
- // Display any staging warnings
525
- displayWarnings(stageResult.warnings);
526
-
527
- // Summary
528
- console.log();
529
- console.log(chalk.green('─'.repeat(48)));
530
- console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
531
- console.log();
532
- console.log(chalk.gray(`Staged to: ${stageResult.stagedPath}`));
533
- console.log();
534
-
535
- // Ask if they want to publish now
536
- const shouldPublish = await confirm({
537
- message: 'Do you want to publish to Magentrix now?',
538
- default: true
539
- });
540
-
541
- if (shouldPublish) {
542
- console.log();
543
- console.log(chalk.blue('Running publish from workspace...'));
544
- console.log();
545
-
546
- const publishSuccess = await runCommandFromWorkspace(workspacePath, 'publish');
547
-
548
- if (!publishSuccess) {
549
- console.log();
550
- console.log(chalk.cyan('To publish manually:'));
551
- console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
552
- console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
553
- }
554
- } else {
555
- console.log();
556
- console.log(chalk.cyan('Next steps:'));
557
- console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
558
- console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
559
- console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
560
- }
561
- }
562
-
563
- /**
564
- * Check if a workspace needs to pull (has remote changes or conflicts).
565
- *
566
- * @param {string} workspacePath - Path to the Magentrix workspace
567
- * @returns {Promise<{checked: boolean, needsPull: boolean, timedOut: boolean}>}
568
- */
569
- async function checkWorkspaceSyncStatus(workspacePath) {
570
- return new Promise((resolvePromise) => {
571
- const isWindows = process.platform === 'win32';
572
- const npmCmd = isWindows ? 'npx.cmd' : 'npx';
573
-
574
- let output = '';
575
- let resolved = false;
576
-
577
- const child = spawn(npmCmd, ['magentrix', 'status'], {
578
- cwd: workspacePath,
579
- stdio: ['inherit', 'pipe', 'pipe'],
580
- shell: isWindows
581
- });
582
-
583
- // Set timeout for status check
584
- const timeout = setTimeout(() => {
585
- if (!resolved) {
586
- resolved = true;
587
- try {
588
- child.kill('SIGTERM');
589
- } catch {
590
- // Ignore kill errors
591
- }
592
- console.log(chalk.yellow(`\n⚠ Status check timed out after ${STATUS_CHECK_TIMEOUT / 1000} seconds`));
593
- resolvePromise({ checked: false, needsPull: false, timedOut: true });
594
- }
595
- }, STATUS_CHECK_TIMEOUT);
596
-
597
- child.stdout.on('data', (data) => {
598
- output += data.toString();
599
- });
600
-
601
- child.stderr.on('data', (data) => {
602
- output += data.toString();
603
- });
604
-
605
- child.on('close', (code) => {
606
- if (resolved) return;
607
- resolved = true;
608
- clearTimeout(timeout);
609
-
610
- // Check output for specific issue indicators from status command
611
- // Note: We avoid checking for "remote" as it appears in normal output
612
- // ("Checking local files vs remote Magentrix...")
613
- const lowerOutput = output.toLowerCase();
614
-
615
- // If everything is in sync, this message appears
616
- const isInSync = lowerOutput.includes('all files are up to date') ||
617
- lowerOutput.includes('in sync');
618
-
619
- // Check for specific issue keywords from logFileStatus output
620
- const hasConflict = lowerOutput.includes('conflict');
621
- const isOutdated = lowerOutput.includes('outdated');
622
- const isAhead = lowerOutput.includes('is ahead');
623
- const isMissing = lowerOutput.includes('is missing');
624
- const hasContentMismatch = lowerOutput.includes('content mismatch');
625
- const hasWarnings = lowerOutput.includes('⚠️') || lowerOutput.includes('🛑');
626
-
627
- const needsPull = !isInSync && (hasConflict || isOutdated || isAhead || isMissing || hasContentMismatch || hasWarnings);
628
-
629
- resolvePromise({ checked: code === 0, needsPull, timedOut: false });
630
- });
631
-
632
- child.on('error', (err) => {
633
- if (resolved) return;
634
- resolved = true;
635
- clearTimeout(timeout);
636
-
637
- // If we can't check, assume it's fine and let them proceed
638
- console.log(chalk.yellow(`\n⚠ Could not check sync status: ${err.message}`));
639
- resolvePromise({ checked: false, needsPull: false, timedOut: false });
640
- });
641
- });
642
- }
643
-
644
- /**
645
- * Run a magentrix command from a specific workspace directory.
646
- *
647
- * @param {string} workspacePath - Path to the Magentrix workspace
648
- * @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
649
- * @param {number} timeout - Optional timeout in milliseconds (defaults to COMMAND_TIMEOUT)
650
- * @returns {Promise<boolean>} - True if command succeeded
651
- */
652
- async function runCommandFromWorkspace(workspacePath, command, timeout = COMMAND_TIMEOUT) {
653
- return new Promise((resolvePromise) => {
654
- const isWindows = process.platform === 'win32';
655
- const npmCmd = isWindows ? 'npx.cmd' : 'npx';
656
-
657
- let resolved = false;
658
-
659
- const child = spawn(npmCmd, ['magentrix', command], {
660
- cwd: workspacePath,
661
- stdio: 'inherit',
662
- shell: isWindows
663
- });
664
-
665
- // Set timeout for command execution
666
- const timeoutId = setTimeout(() => {
667
- if (!resolved) {
668
- resolved = true;
669
- try {
670
- child.kill('SIGTERM');
671
- } catch {
672
- // Ignore kill errors
673
- }
674
- console.log();
675
- console.log(chalk.red(formatTimeoutError({
676
- operation: `magentrix ${command}`,
677
- timeout: timeout,
678
- suggestion: `The ${command} operation is taking too long. Check your network connection or try again later.`
679
- })));
680
- resolvePromise(false);
681
- }
682
- }, timeout);
683
-
684
- child.on('close', (code) => {
685
- if (resolved) return;
686
- resolved = true;
687
- clearTimeout(timeoutId);
688
- resolvePromise(code === 0);
689
- });
690
-
691
- child.on('error', (err) => {
692
- if (resolved) return;
693
- resolved = true;
694
- clearTimeout(timeoutId);
695
- console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
696
- resolvePromise(false);
697
- });
698
- });
699
- }
700
-
701
- /**
702
- * Prompt user to select a Magentrix workspace.
703
- *
704
- * @returns {Promise<string | null>} - Selected workspace path or null if cancelled
705
- */
706
- async function selectWorkspace() {
707
- const workspaces = getValidWorkspaces();
708
-
709
- if (workspaces.length === 0) {
710
- console.log(chalk.yellow('No Magentrix workspaces found.'));
711
- console.log();
712
- console.log(chalk.gray('To register a workspace:'));
713
- console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
714
- console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
715
- console.log();
716
- console.log(chalk.gray('Or specify a workspace path directly:'));
717
- console.log(chalk.white(` ${chalk.cyan('magentrix vue-run-build --workspace /path/to/workspace')}`));
718
- console.log();
719
-
720
- // Allow manual entry
721
- const manualPath = await input({
722
- message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
723
- validate: (value) => {
724
- if (!value.trim()) return true; // Allow empty for cancel
725
- const resolved = resolve(value);
726
- if (!existsSync(resolved)) {
727
- return `Path does not exist: ${resolved}`;
728
- }
729
- const magentrixFolder = join(resolved, '.magentrix');
730
- if (!existsSync(magentrixFolder)) {
731
- return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
732
- }
733
- return true;
734
- }
735
- });
736
-
737
- if (!manualPath.trim()) {
738
- console.log(chalk.gray('Cancelled.'));
739
- return null;
740
- }
741
-
742
- return resolve(manualPath);
743
- }
744
-
745
- // Build choices from registered workspaces
746
- const choices = workspaces.map(w => ({
747
- name: `${w.path}`,
748
- value: w.path,
749
- description: chalk.dim(`→ ${w.instanceUrl}`)
750
- }));
751
-
752
- choices.push({
753
- name: 'Enter path manually',
754
- value: '__manual__',
755
- description: chalk.dim('→ Specify the full path to a workspace')
756
- });
757
-
758
- choices.push({
759
- name: 'Cancel',
760
- value: '__cancel__'
761
- });
762
-
763
- const choice = await select({
764
- message: 'Which workspace do you want to stage into?',
765
- choices
766
- });
767
-
768
- if (choice === '__cancel__') {
769
- console.log(chalk.gray('Cancelled.'));
770
- return null;
771
- }
772
-
773
- if (choice === '__manual__') {
774
- const manualPath = await input({
775
- message: 'Enter the path to your Magentrix workspace:',
776
- validate: (value) => {
777
- if (!value.trim()) {
778
- return 'Path is required';
779
- }
780
- const resolved = resolve(value);
781
- if (!existsSync(resolved)) {
782
- return `Path does not exist: ${resolved}`;
783
- }
784
- const magentrixFolder = join(resolved, '.magentrix');
785
- if (!existsSync(magentrixFolder)) {
786
- return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
787
- }
788
- return true;
789
- }
790
- });
791
-
792
- return resolve(manualPath);
793
- }
794
-
795
- return choice;
796
- }
797
-
798
- /**
799
- * Prompt user to select a project.
800
- *
801
- * @returns {Promise<{path: string, config: object} | null>}
802
- */
803
- async function selectProject() {
804
- const projectsWithStatus = getLinkedProjectsWithStatus();
805
-
806
- // Check if CWD is a Vue project (and not already linked)
807
- const cwdConfig = readVueConfig(process.cwd());
808
- const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
809
- const cwdAlreadyLinked = projectsWithStatus.some(p =>
810
- resolve(p.path) === resolve(process.cwd())
811
- );
812
-
813
- // Build choices using the helper
814
- const choices = buildProjectChoices({
815
- includeManual: true,
816
- includeCancel: true,
817
- showInvalid: true
818
- });
819
-
820
- // If CWD is a valid Vue project and not linked, add it at the top
821
- if (inVueProject && !cwdAlreadyLinked) {
822
- choices.unshift({
823
- name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
824
- value: { type: 'cwd', path: process.cwd() }
825
- });
826
- }
827
-
828
- // Check if we have any projects to show
829
- const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
830
-
831
- if (!hasProjects) {
832
- console.log(chalk.yellow('No linked Vue projects found.'));
833
- console.log();
834
- console.log(chalk.gray('To get started:'));
835
- console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
836
- console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-build --path /path/to/vue-project')}`));
837
- console.log();
838
- }
839
-
840
- const choice = await select({
841
- message: 'Which project do you want to build?',
842
- choices
843
- });
844
-
845
- if (choice.type === 'cancel') {
846
- console.log(chalk.gray('Cancelled.'));
847
- return null;
848
- }
849
-
850
- if (choice.type === 'manual') {
851
- const manualPath = await input({
852
- message: 'Enter the path to your Vue project:',
853
- validate: (value) => {
854
- if (!value.trim()) {
855
- return 'Path is required';
856
- }
857
- const resolved = resolve(value);
858
- if (!existsSync(resolved)) {
859
- return `Path does not exist: ${resolved}`;
860
- }
861
- return true;
862
- }
863
- });
864
-
865
- const config = readVueConfig(resolve(manualPath));
866
- return { path: resolve(manualPath), config };
867
- }
868
-
869
- // Linked project or CWD
870
- const config = readVueConfig(choice.path);
871
- return { path: choice.path, config };
872
- }
873
-
874
- export default vueBuildStage;
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 { spawn } from 'node:child_process';
6
+ import Config from '../../utils/config.js';
7
+ import { runPublish } from '../publish.js';
8
+ import { isAutopublishRunning } from '../../utils/autopublishLock.js';
9
+ import {
10
+ buildVueProject,
11
+ stageToWorkspace,
12
+ findDistDirectory,
13
+ formatBuildError,
14
+ formatValidationError
15
+ } from '../../utils/iris/builder.js';
16
+ import { validateIrisBuild } from '../../utils/iris/validator.js';
17
+ import {
18
+ readVueConfig,
19
+ formatMissingConfigError,
20
+ formatConfigErrors
21
+ } from '../../utils/iris/config-reader.js';
22
+ import {
23
+ getLinkedProjectsWithStatus,
24
+ linkVueProject,
25
+ findLinkedProjectByPath,
26
+ buildProjectChoices
27
+ } from '../../utils/iris/linker.js';
28
+ import { EXPORT_ROOT, IRIS_APPS_DIR, HASHED_CWD } from '../../vars/global.js';
29
+ import { getValidWorkspaces } from '../../utils/workspaces.js';
30
+ import { formatTimeoutError } from '../../utils/iris/errors.js';
31
+
32
+ const config = new Config();
33
+
34
+ /**
35
+ * Timeout for status check operations (30 seconds).
36
+ */
37
+ const STATUS_CHECK_TIMEOUT = 30000;
38
+
39
+ /**
40
+ * Timeout for command execution (5 minutes).
41
+ */
42
+ const COMMAND_TIMEOUT = 5 * 60 * 1000;
43
+
44
+ /**
45
+ * Display warnings from build/stage operations.
46
+ *
47
+ * @param {string[]} warnings - Array of warning messages
48
+ */
49
+ function displayWarnings(warnings) {
50
+ if (!warnings || warnings.length === 0) return;
51
+
52
+ for (const warning of warnings) {
53
+ console.log(chalk.yellow(`⚠ ${warning}`));
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check if the current directory is a Vue project (has config.ts).
59
+ * @returns {boolean}
60
+ */
61
+ function isInVueProject() {
62
+ const configLocations = [
63
+ 'src/config.ts',
64
+ 'config.ts',
65
+ 'src/iris-config.ts',
66
+ 'iris-config.ts'
67
+ ];
68
+ return configLocations.some(loc => existsSync(join(process.cwd(), loc)));
69
+ }
70
+
71
+ /**
72
+ * Check if the current directory appears to be a Magentrix workspace.
73
+ * @returns {boolean} - True if in a Magentrix workspace
74
+ */
75
+ function isMagentrixWorkspace() {
76
+ const magentrixFolder = join(process.cwd(), '.magentrix');
77
+ const srcFolder = join(process.cwd(), 'src');
78
+
79
+ // Check for .magentrix folder
80
+ if (!existsSync(magentrixFolder)) return false;
81
+
82
+ // Check for src folder (typical workspace structure)
83
+ if (!existsSync(srcFolder)) return false;
84
+
85
+ // Check if credentials are configured (instanceUrl or apiKey in global config)
86
+ const instanceUrl = config.read('instanceUrl', { global: true, pathHash: HASHED_CWD });
87
+ const apiKey = config.read('apiKey', { global: true, pathHash: HASHED_CWD });
88
+
89
+ return !!(instanceUrl || apiKey);
90
+ }
91
+
92
+ /**
93
+ * vue-run-build command - Build a Vue project and stage to CLI workspace.
94
+ *
95
+ * Two modes of operation:
96
+ * 1. Run from Magentrix workspace: prompts for which Vue project to build
97
+ * 2. Run from Vue project: prompts for which workspace to stage into
98
+ *
99
+ * Options:
100
+ * --path <dir> Specify Vue project path directly
101
+ * --skip-build Use existing dist/ without rebuilding
102
+ * --workspace <dir> Specify Magentrix workspace path directly
103
+ */
104
+ export const vueBuildStage = async (options = {}) => {
105
+ process.stdout.write('\x1Bc'); // Clear console
106
+
107
+ const { path: pathOption, skipBuild, workspace: workspaceOption } = options;
108
+
109
+ // Detect which mode we're in
110
+ const inVueProject = isInVueProject();
111
+ const inWorkspace = isMagentrixWorkspace();
112
+
113
+ // If run from a Vue project, use reversed logic
114
+ if (inVueProject && !inWorkspace) {
115
+ await buildFromVueProject(options);
116
+ return;
117
+ }
118
+
119
+ // Standard mode: run from workspace, select Vue project
120
+ // Warn if not in a Magentrix workspace
121
+ if (!inWorkspace) {
122
+ console.log(chalk.yellow('⚠ Warning: Magentrix Workspace Not Detected'));
123
+ console.log(chalk.gray('─'.repeat(48)));
124
+ console.log(chalk.white('\nThis command should be run from your Magentrix CLI workspace directory.'));
125
+ console.log(chalk.white('It stages Vue.js build files to ') + chalk.cyan('src/iris-apps/<slug>/'));
126
+ console.log();
127
+ console.log(chalk.white('Expected workspace indicators:'));
128
+ console.log(chalk.gray(' • .magentrix/ folder (config and cache)'));
129
+ console.log(chalk.gray(' • src/ folder (code files and assets)'));
130
+ console.log(chalk.gray(' • Magentrix credentials configured'));
131
+ console.log();
132
+ console.log(chalk.white('Current directory: ') + chalk.gray(process.cwd()));
133
+ console.log();
134
+
135
+ const shouldContinue = await confirm({
136
+ message: 'Do you want to continue anyway?',
137
+ default: false
138
+ });
139
+
140
+ if (!shouldContinue) {
141
+ console.log(chalk.gray('\nCancelled. Run this command from your Magentrix workspace.'));
142
+ return;
143
+ }
144
+
145
+ console.log();
146
+ }
147
+
148
+ // Determine which project to build
149
+ let projectPath = pathOption;
150
+ let vueConfig = null;
151
+
152
+ if (projectPath) {
153
+ // Path provided via option
154
+ projectPath = resolve(projectPath);
155
+
156
+ if (!existsSync(projectPath)) {
157
+ console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
158
+ return;
159
+ }
160
+
161
+ vueConfig = readVueConfig(projectPath);
162
+ } else {
163
+ // Prompt user to select a project
164
+ const result = await selectProject();
165
+ if (!result) return; // User cancelled
166
+
167
+ projectPath = result.path;
168
+ vueConfig = result.config;
169
+ }
170
+
171
+ // Validate Vue config
172
+ if (!vueConfig.found) {
173
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
174
+ return;
175
+ }
176
+
177
+ if (vueConfig.errors.length > 0) {
178
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
179
+ return;
180
+ }
181
+
182
+ const { slug, appName } = vueConfig;
183
+
184
+ console.log(chalk.blue('\nVue Build & Stage'));
185
+ console.log(chalk.gray('─'.repeat(48)));
186
+ console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
187
+ console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
188
+ console.log();
189
+
190
+ // Ensure project is linked
191
+ const linked = findLinkedProjectByPath(projectPath);
192
+ if (!linked) {
193
+ const shouldLink = await confirm({
194
+ message: 'This project is not linked. Link it now?',
195
+ default: true
196
+ });
197
+
198
+ if (shouldLink) {
199
+ const linkResult = linkVueProject(projectPath);
200
+ if (!linkResult.success) {
201
+ console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
202
+ return;
203
+ }
204
+ console.log(chalk.green(`\u2713 Project linked`));
205
+ }
206
+ }
207
+
208
+ let distPath;
209
+
210
+ if (skipBuild) {
211
+ // Use existing dist
212
+ distPath = findDistDirectory(projectPath);
213
+
214
+ if (!distPath) {
215
+ console.log(chalk.red('No existing dist/ directory found.'));
216
+ console.log(chalk.gray('Run without --skip-build to build the project.'));
217
+ return;
218
+ }
219
+
220
+ console.log(chalk.yellow(`Using existing dist: ${distPath}`));
221
+
222
+ // Validate the existing build
223
+ const validation = validateIrisBuild(distPath);
224
+ if (!validation.valid) {
225
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
226
+ return;
227
+ }
228
+
229
+ console.log(chalk.green('\u2713 Existing build is valid'));
230
+ } else {
231
+ // Build the project
232
+ console.log(chalk.blue('Building project...'));
233
+ console.log();
234
+
235
+ const buildResult = await buildVueProject(projectPath, { silent: false });
236
+
237
+ if (!buildResult.success) {
238
+ console.log();
239
+ console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
240
+ return;
241
+ }
242
+
243
+ distPath = buildResult.distPath;
244
+ console.log();
245
+ console.log(chalk.green(`\u2713 Build completed successfully`));
246
+ console.log(chalk.gray(` Output: ${distPath}`));
247
+
248
+ // Display any build warnings
249
+ displayWarnings(buildResult.warnings);
250
+
251
+ // Validate build output
252
+ const validation = validateIrisBuild(distPath);
253
+ if (!validation.valid) {
254
+ console.log();
255
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
256
+ return;
257
+ }
258
+
259
+ console.log(chalk.green('\u2713 Build output validated'));
260
+ }
261
+
262
+ // Stage to CLI project (current workspace)
263
+ console.log();
264
+ console.log(chalk.blue('Staging to CLI workspace...'));
265
+
266
+ const stageResult = stageToWorkspace(distPath, slug, process.cwd());
267
+
268
+ if (!stageResult.success) {
269
+ console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
270
+ return;
271
+ }
272
+
273
+ console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
274
+
275
+ // Display any staging warnings
276
+ displayWarnings(stageResult.warnings);
277
+
278
+ // Summary
279
+ console.log();
280
+ console.log(chalk.green('─'.repeat(48)));
281
+ console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
282
+ console.log();
283
+ console.log(chalk.gray(`Staged to: ${EXPORT_ROOT}/${IRIS_APPS_DIR}/${slug}/`));
284
+ console.log();
285
+
286
+ // Check if autopublish is running
287
+ if (isAutopublishRunning()) {
288
+ console.log(chalk.cyan('✓ Autopublish is running - changes will be deployed automatically'));
289
+ } else {
290
+ // Prompt to run publish now
291
+ const shouldPublish = await confirm({
292
+ message: 'Do you want to publish to Magentrix now?',
293
+ default: true
294
+ });
295
+
296
+ if (shouldPublish) {
297
+ console.log();
298
+ console.log(chalk.blue('Running publish...'));
299
+ console.log();
300
+
301
+ try {
302
+ await runPublish();
303
+ } catch (error) {
304
+ console.log(chalk.red(`\nPublish failed: ${error.message}`));
305
+ console.log(chalk.gray('You can run it manually later with:'), chalk.yellow('magentrix publish'));
306
+ }
307
+ } else {
308
+ console.log();
309
+ console.log(chalk.cyan('Next steps:'));
310
+ console.log(chalk.white(` • Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
311
+ console.log(chalk.white(` • Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
312
+ }
313
+ }
314
+ };
315
+
316
+ /**
317
+ * Build and stage when running from inside a Vue project.
318
+ * Prompts user to select which workspace to stage into.
319
+ */
320
+ async function buildFromVueProject(options) {
321
+ const { skipBuild, workspace: workspaceOption } = options;
322
+
323
+ // Use current directory as Vue project
324
+ const projectPath = process.cwd();
325
+ const vueConfig = readVueConfig(projectPath);
326
+
327
+ // Validate Vue config
328
+ if (!vueConfig.found) {
329
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
330
+ return;
331
+ }
332
+
333
+ if (vueConfig.errors.length > 0) {
334
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
335
+ return;
336
+ }
337
+
338
+ const { slug, appName } = vueConfig;
339
+
340
+ console.log(chalk.blue('\nVue Build & Stage'));
341
+ console.log(chalk.gray('─'.repeat(48)));
342
+ console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
343
+ console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
344
+ console.log();
345
+
346
+ // Determine which workspace to stage into
347
+ let workspacePath = workspaceOption;
348
+
349
+ if (workspacePath) {
350
+ workspacePath = resolve(workspacePath);
351
+ if (!existsSync(workspacePath)) {
352
+ console.log(chalk.red(`Error: Workspace path does not exist: ${workspacePath}`));
353
+ return;
354
+ }
355
+ } else {
356
+ // Prompt user to select a workspace
357
+ const result = await selectWorkspace();
358
+ if (!result) return; // User cancelled
359
+ workspacePath = result;
360
+ }
361
+
362
+ // Ensure project is linked
363
+ const linked = findLinkedProjectByPath(projectPath);
364
+ if (!linked) {
365
+ const shouldLink = await confirm({
366
+ message: 'This project is not linked. Link it now?',
367
+ default: true
368
+ });
369
+
370
+ if (shouldLink) {
371
+ const linkResult = linkVueProject(projectPath);
372
+ if (!linkResult.success) {
373
+ console.log(chalk.red(`Failed to link project: ${linkResult.error}`));
374
+ return;
375
+ }
376
+ console.log(chalk.green(`\u2713 Project linked`));
377
+ }
378
+ }
379
+
380
+ let distPath;
381
+
382
+ if (skipBuild) {
383
+ // Use existing dist
384
+ distPath = findDistDirectory(projectPath);
385
+
386
+ if (!distPath) {
387
+ console.log(chalk.red('No existing dist/ directory found.'));
388
+ console.log(chalk.gray('Run without --skip-build to build the project.'));
389
+ return;
390
+ }
391
+
392
+ console.log(chalk.yellow(`Using existing dist: ${distPath}`));
393
+
394
+ // Validate the existing build
395
+ const validation = validateIrisBuild(distPath);
396
+ if (!validation.valid) {
397
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
398
+ return;
399
+ }
400
+
401
+ console.log(chalk.green('\u2713 Existing build is valid'));
402
+ } else {
403
+ // Build the project
404
+ console.log(chalk.blue('Building project...'));
405
+ console.log();
406
+
407
+ const buildResult = await buildVueProject(projectPath, { silent: false });
408
+
409
+ if (!buildResult.success) {
410
+ console.log();
411
+ console.log(chalk.red(formatBuildError(projectPath, buildResult.error)));
412
+ return;
413
+ }
414
+
415
+ distPath = buildResult.distPath;
416
+ console.log();
417
+ console.log(chalk.green(`\u2713 Build completed successfully`));
418
+ console.log(chalk.gray(` Output: ${distPath}`));
419
+
420
+ // Display any build warnings
421
+ displayWarnings(buildResult.warnings);
422
+
423
+ // Validate build output
424
+ const validation = validateIrisBuild(distPath);
425
+ if (!validation.valid) {
426
+ console.log();
427
+ console.log(chalk.red(formatValidationError(distPath, validation.errors)));
428
+ return;
429
+ }
430
+
431
+ console.log(chalk.green('\u2713 Build output validated'));
432
+ }
433
+
434
+ // Check if workspace might be out of sync and offer to pull first (BEFORE staging)
435
+ // This ensures pull doesn't overwrite our staged files
436
+ console.log();
437
+ console.log(chalk.gray('Checking workspace sync status...'));
438
+
439
+ // Check for a previous incomplete pull first — `magentrix status` only checks
440
+ // code entities, so a partial pull that synced code but not assets would falsely
441
+ // report "in sync". The marker file catches this case.
442
+ const workspaceConfig = new Config({ projectDir: workspacePath });
443
+ const hadIncompletePull = workspaceConfig.read('pullIncomplete', { global: false, filename: 'config.json' });
444
+
445
+ const syncStatus = await checkWorkspaceSyncStatus(workspacePath);
446
+ const needsPull = syncStatus.needsPull || !!hadIncompletePull;
447
+
448
+ if (needsPull) {
449
+ console.log();
450
+ if (hadIncompletePull && !syncStatus.needsPull) {
451
+ console.log(chalk.yellow('⚠ A previous pull did not complete. Your workspace may be out of sync.'));
452
+ } else {
453
+ console.log(chalk.yellow('⚠ Your workspace may be out of sync with the server.'));
454
+ }
455
+
456
+ const shouldPull = await confirm({
457
+ message: 'Would you like to pull latest changes first?',
458
+ default: true
459
+ });
460
+
461
+ if (shouldPull) {
462
+ console.log();
463
+ console.log(chalk.blue('Running pull from workspace...'));
464
+ console.log();
465
+
466
+ // Mark pull as in-progress before starting
467
+ workspaceConfig.save('pullIncomplete', true, { global: false, filename: 'config.json' });
468
+
469
+ const pullSuccess = await runCommandFromWorkspace(workspacePath, 'pull');
470
+
471
+ if (pullSuccess) {
472
+ // Pull completed successfully — clear the marker
473
+ workspaceConfig.removeKey('pullIncomplete', { filename: 'config.json' });
474
+ } else {
475
+ // Pull failed or was cancelled — marker stays for next run
476
+ console.log();
477
+ console.log(chalk.yellow('Pull encountered issues. You may want to resolve them manually.'));
478
+
479
+ const continueAnyway = await confirm({
480
+ message: 'Do you still want to continue with staging and publishing?',
481
+ default: false
482
+ });
483
+
484
+ if (!continueAnyway) {
485
+ console.log();
486
+ console.log(chalk.cyan('To continue manually:'));
487
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
488
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to resolve conflicts`));
489
+ console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix vue-run-build --skip-build')} to stage`));
490
+ console.log(chalk.white(` 4. Run ${chalk.yellow('magentrix publish')} to deploy`));
491
+ return;
492
+ }
493
+ }
494
+ console.log();
495
+ } else {
496
+ // User declined to pull - block publishing from out-of-sync workspace
497
+ console.log();
498
+ console.log(chalk.red('Publishing Iris apps from an out-of-sync workspace is not allowed.'));
499
+ console.log(chalk.gray('This prevents conflicts and ensures your deployment is based on the latest server state.'));
500
+ console.log();
501
+ console.log(chalk.cyan('To continue:'));
502
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
503
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix pull')} to sync with server`));
504
+ console.log(chalk.white(` 3. Run ${chalk.yellow('magentrix publish')} to deploy`));
505
+ return;
506
+ }
507
+ } else if (syncStatus.checked) {
508
+ console.log(chalk.green('✓ Workspace is in sync'));
509
+ }
510
+
511
+ // Stage to selected workspace (AFTER pull to avoid overwriting)
512
+ console.log();
513
+ console.log(chalk.blue(`Staging to workspace: ${workspacePath}`));
514
+
515
+ const stageResult = stageToWorkspace(distPath, slug, workspacePath);
516
+
517
+ if (!stageResult.success) {
518
+ console.log(chalk.red(`Failed to stage: ${stageResult.error}`));
519
+ return;
520
+ }
521
+
522
+ console.log(chalk.green(`\u2713 Staged ${stageResult.fileCount} files to ${stageResult.stagedPath}`));
523
+
524
+ // Display any staging warnings
525
+ displayWarnings(stageResult.warnings);
526
+
527
+ // Summary
528
+ console.log();
529
+ console.log(chalk.green('─'.repeat(48)));
530
+ console.log(chalk.green.bold('\u2713 Build & Stage Complete!'));
531
+ console.log();
532
+ console.log(chalk.gray(`Staged to: ${stageResult.stagedPath}`));
533
+ console.log();
534
+
535
+ // Ask if they want to publish now
536
+ const shouldPublish = await confirm({
537
+ message: 'Do you want to publish to Magentrix now?',
538
+ default: true
539
+ });
540
+
541
+ if (shouldPublish) {
542
+ console.log();
543
+ console.log(chalk.blue('Running publish from workspace...'));
544
+ console.log();
545
+
546
+ const publishSuccess = await runCommandFromWorkspace(workspacePath, 'publish');
547
+
548
+ if (!publishSuccess) {
549
+ console.log();
550
+ console.log(chalk.cyan('To publish manually:'));
551
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
552
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')}`));
553
+ }
554
+ } else {
555
+ console.log();
556
+ console.log(chalk.cyan('Next steps:'));
557
+ console.log(chalk.white(` 1. Navigate to workspace: ${chalk.yellow(`cd "${workspacePath}"`)}`));
558
+ console.log(chalk.white(` 2. Run ${chalk.yellow('magentrix publish')} to deploy to Magentrix`));
559
+ console.log(chalk.white(` Or use ${chalk.yellow('magentrix autopublish')} for automatic deployment`));
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Check if a workspace needs to pull (has remote changes or conflicts).
565
+ *
566
+ * @param {string} workspacePath - Path to the Magentrix workspace
567
+ * @returns {Promise<{checked: boolean, needsPull: boolean, timedOut: boolean}>}
568
+ */
569
+ async function checkWorkspaceSyncStatus(workspacePath) {
570
+ return new Promise((resolvePromise) => {
571
+ const isWindows = process.platform === 'win32';
572
+ const npmCmd = isWindows ? 'npx.cmd' : 'npx';
573
+
574
+ let output = '';
575
+ let resolved = false;
576
+
577
+ const child = spawn(npmCmd, ['magentrix', 'status'], {
578
+ cwd: workspacePath,
579
+ stdio: ['inherit', 'pipe', 'pipe'],
580
+ shell: isWindows
581
+ });
582
+
583
+ // Set timeout for status check
584
+ const timeout = setTimeout(() => {
585
+ if (!resolved) {
586
+ resolved = true;
587
+ try {
588
+ child.kill('SIGTERM');
589
+ } catch {
590
+ // Ignore kill errors
591
+ }
592
+ console.log(chalk.yellow(`\n⚠ Status check timed out after ${STATUS_CHECK_TIMEOUT / 1000} seconds`));
593
+ resolvePromise({ checked: false, needsPull: false, timedOut: true });
594
+ }
595
+ }, STATUS_CHECK_TIMEOUT);
596
+
597
+ child.stdout.on('data', (data) => {
598
+ output += data.toString();
599
+ });
600
+
601
+ child.stderr.on('data', (data) => {
602
+ output += data.toString();
603
+ });
604
+
605
+ child.on('close', (code) => {
606
+ if (resolved) return;
607
+ resolved = true;
608
+ clearTimeout(timeout);
609
+
610
+ // Check output for specific issue indicators from status command
611
+ // Note: We avoid checking for "remote" as it appears in normal output
612
+ // ("Checking local files vs remote Magentrix...")
613
+ const lowerOutput = output.toLowerCase();
614
+
615
+ // If everything is in sync, this message appears
616
+ const isInSync = lowerOutput.includes('all files are up to date') ||
617
+ lowerOutput.includes('in sync');
618
+
619
+ // Check for specific issue keywords from logFileStatus output
620
+ const hasConflict = lowerOutput.includes('conflict');
621
+ const isOutdated = lowerOutput.includes('outdated');
622
+ const isAhead = lowerOutput.includes('is ahead');
623
+ const isMissing = lowerOutput.includes('is missing');
624
+ const hasContentMismatch = lowerOutput.includes('content mismatch');
625
+ const hasWarnings = lowerOutput.includes('⚠️') || lowerOutput.includes('🛑');
626
+
627
+ const needsPull = !isInSync && (hasConflict || isOutdated || isAhead || isMissing || hasContentMismatch || hasWarnings);
628
+
629
+ resolvePromise({ checked: code === 0, needsPull, timedOut: false });
630
+ });
631
+
632
+ child.on('error', (err) => {
633
+ if (resolved) return;
634
+ resolved = true;
635
+ clearTimeout(timeout);
636
+
637
+ // If we can't check, assume it's fine and let them proceed
638
+ console.log(chalk.yellow(`\n⚠ Could not check sync status: ${err.message}`));
639
+ resolvePromise({ checked: false, needsPull: false, timedOut: false });
640
+ });
641
+ });
642
+ }
643
+
644
+ /**
645
+ * Run a magentrix command from a specific workspace directory.
646
+ *
647
+ * @param {string} workspacePath - Path to the Magentrix workspace
648
+ * @param {string} command - The magentrix command to run (e.g., 'pull', 'publish')
649
+ * @param {number} timeout - Optional timeout in milliseconds (defaults to COMMAND_TIMEOUT)
650
+ * @returns {Promise<boolean>} - True if command succeeded
651
+ */
652
+ async function runCommandFromWorkspace(workspacePath, command, timeout = COMMAND_TIMEOUT) {
653
+ return new Promise((resolvePromise) => {
654
+ const isWindows = process.platform === 'win32';
655
+ const npmCmd = isWindows ? 'npx.cmd' : 'npx';
656
+
657
+ let resolved = false;
658
+
659
+ const child = spawn(npmCmd, ['magentrix', command], {
660
+ cwd: workspacePath,
661
+ stdio: 'inherit',
662
+ shell: isWindows
663
+ });
664
+
665
+ // Set timeout for command execution
666
+ const timeoutId = setTimeout(() => {
667
+ if (!resolved) {
668
+ resolved = true;
669
+ try {
670
+ child.kill('SIGTERM');
671
+ } catch {
672
+ // Ignore kill errors
673
+ }
674
+ console.log();
675
+ console.log(chalk.red(formatTimeoutError({
676
+ operation: `magentrix ${command}`,
677
+ timeout: timeout,
678
+ suggestion: `The ${command} operation is taking too long. Check your network connection or try again later.`
679
+ })));
680
+ resolvePromise(false);
681
+ }
682
+ }, timeout);
683
+
684
+ child.on('close', (code) => {
685
+ if (resolved) return;
686
+ resolved = true;
687
+ clearTimeout(timeoutId);
688
+ resolvePromise(code === 0);
689
+ });
690
+
691
+ child.on('error', (err) => {
692
+ if (resolved) return;
693
+ resolved = true;
694
+ clearTimeout(timeoutId);
695
+ console.log(chalk.yellow(`Warning: Could not run ${command}: ${err.message}`));
696
+ resolvePromise(false);
697
+ });
698
+ });
699
+ }
700
+
701
+ /**
702
+ * Prompt user to select a Magentrix workspace.
703
+ *
704
+ * @returns {Promise<string | null>} - Selected workspace path or null if cancelled
705
+ */
706
+ async function selectWorkspace() {
707
+ const workspaces = getValidWorkspaces();
708
+
709
+ if (workspaces.length === 0) {
710
+ console.log(chalk.yellow('No Magentrix workspaces found.'));
711
+ console.log();
712
+ console.log(chalk.gray('To register a workspace:'));
713
+ console.log(chalk.white(` • Run ${chalk.cyan('magentrix')} from an existing workspace (auto-registers it)`));
714
+ console.log(chalk.white(` • Or run ${chalk.cyan('magentrix setup')} in a new directory to create one`));
715
+ console.log();
716
+ console.log(chalk.gray('Or specify a workspace path directly:'));
717
+ console.log(chalk.white(` ${chalk.cyan('magentrix vue-run-build --workspace /path/to/workspace')}`));
718
+ console.log();
719
+
720
+ // Allow manual entry
721
+ const manualPath = await input({
722
+ message: 'Enter the path to your Magentrix workspace (or leave empty to cancel):',
723
+ validate: (value) => {
724
+ if (!value.trim()) return true; // Allow empty for cancel
725
+ const resolved = resolve(value);
726
+ if (!existsSync(resolved)) {
727
+ return `Path does not exist: ${resolved}`;
728
+ }
729
+ const magentrixFolder = join(resolved, '.magentrix');
730
+ if (!existsSync(magentrixFolder)) {
731
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
732
+ }
733
+ return true;
734
+ }
735
+ });
736
+
737
+ if (!manualPath.trim()) {
738
+ console.log(chalk.gray('Cancelled.'));
739
+ return null;
740
+ }
741
+
742
+ return resolve(manualPath);
743
+ }
744
+
745
+ // Build choices from registered workspaces
746
+ const choices = workspaces.map(w => ({
747
+ name: `${w.path}`,
748
+ value: w.path,
749
+ description: chalk.dim(`→ ${w.instanceUrl}`)
750
+ }));
751
+
752
+ choices.push({
753
+ name: 'Enter path manually',
754
+ value: '__manual__',
755
+ description: chalk.dim('→ Specify the full path to a workspace')
756
+ });
757
+
758
+ choices.push({
759
+ name: 'Cancel',
760
+ value: '__cancel__'
761
+ });
762
+
763
+ const choice = await select({
764
+ message: 'Which workspace do you want to stage into?',
765
+ choices
766
+ });
767
+
768
+ if (choice === '__cancel__') {
769
+ console.log(chalk.gray('Cancelled.'));
770
+ return null;
771
+ }
772
+
773
+ if (choice === '__manual__') {
774
+ const manualPath = await input({
775
+ message: 'Enter the path to your Magentrix workspace:',
776
+ validate: (value) => {
777
+ if (!value.trim()) {
778
+ return 'Path is required';
779
+ }
780
+ const resolved = resolve(value);
781
+ if (!existsSync(resolved)) {
782
+ return `Path does not exist: ${resolved}`;
783
+ }
784
+ const magentrixFolder = join(resolved, '.magentrix');
785
+ if (!existsSync(magentrixFolder)) {
786
+ return `Not a Magentrix workspace (missing .magentrix folder): ${resolved}`;
787
+ }
788
+ return true;
789
+ }
790
+ });
791
+
792
+ return resolve(manualPath);
793
+ }
794
+
795
+ return choice;
796
+ }
797
+
798
+ /**
799
+ * Prompt user to select a project.
800
+ *
801
+ * @returns {Promise<{path: string, config: object} | null>}
802
+ */
803
+ async function selectProject() {
804
+ const projectsWithStatus = getLinkedProjectsWithStatus();
805
+
806
+ // Check if CWD is a Vue project (and not already linked)
807
+ const cwdConfig = readVueConfig(process.cwd());
808
+ const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
809
+ const cwdAlreadyLinked = projectsWithStatus.some(p =>
810
+ resolve(p.path) === resolve(process.cwd())
811
+ );
812
+
813
+ // Build choices using the helper
814
+ const choices = buildProjectChoices({
815
+ includeManual: true,
816
+ includeCancel: true,
817
+ showInvalid: true
818
+ });
819
+
820
+ // If CWD is a valid Vue project and not linked, add it at the top
821
+ if (inVueProject && !cwdAlreadyLinked) {
822
+ choices.unshift({
823
+ name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
824
+ value: { type: 'cwd', path: process.cwd() }
825
+ });
826
+ }
827
+
828
+ // Check if we have any projects to show
829
+ const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
830
+
831
+ if (!hasProjects) {
832
+ console.log(chalk.yellow('No linked Vue projects found.'));
833
+ console.log();
834
+ console.log(chalk.gray('To get started:'));
835
+ console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
836
+ console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-build --path /path/to/vue-project')}`));
837
+ console.log();
838
+ }
839
+
840
+ const choice = await select({
841
+ message: 'Which project do you want to build?',
842
+ choices
843
+ });
844
+
845
+ if (choice.type === 'cancel') {
846
+ console.log(chalk.gray('Cancelled.'));
847
+ return null;
848
+ }
849
+
850
+ if (choice.type === 'manual') {
851
+ const manualPath = await input({
852
+ message: 'Enter the path to your Vue project:',
853
+ validate: (value) => {
854
+ if (!value.trim()) {
855
+ return 'Path is required';
856
+ }
857
+ const resolved = resolve(value);
858
+ if (!existsSync(resolved)) {
859
+ return `Path does not exist: ${resolved}`;
860
+ }
861
+ return true;
862
+ }
863
+ });
864
+
865
+ const config = readVueConfig(resolve(manualPath));
866
+ return { path: resolve(manualPath), config };
867
+ }
868
+
869
+ // Linked project or CWD
870
+ const config = readVueConfig(choice.path);
871
+ return { path: choice.path, config };
872
+ }
873
+
874
+ export default vueBuildStage;