@magentrix-corp/magentrix-cli 1.3.16 → 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 -74
  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,541 +1,541 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from 'node:fs';
3
- import { join, resolve, basename } from 'node:path';
4
- import { validateIrisBuild } from './validator.js';
5
- import { updateLastBuild } from './linker.js';
6
- import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
7
- import { detectErrorType, ErrorTypes, formatPermissionError, formatDiskFullError, formatFileLockError } from './errors.js';
8
- import { acquireLock, releaseLock, LockTypes, createProjectContext } from './lock.js';
9
-
10
- // Files to exclude when staging Iris apps (not needed on server)
11
- const EXCLUDED_FILES = new Set([
12
- 'index.html',
13
- 'favicon.ico',
14
- '.DS_Store',
15
- 'Thumbs.db',
16
- '.gitignore',
17
- '.gitkeep'
18
- ]);
19
-
20
- /**
21
- * Maximum dist folder size before warning (500MB)
22
- */
23
- const MAX_DIST_SIZE_WARNING = 500 * 1024 * 1024;
24
-
25
- /**
26
- * Build a Vue project using npm run build.
27
- *
28
- * @param {string} projectPath - Path to the Vue project
29
- * @param {Object} options - Build options
30
- * @param {boolean} options.silent - Suppress build output
31
- * @returns {Promise<{
32
- * success: boolean,
33
- * output: string,
34
- * error: string | null,
35
- * distPath: string | null,
36
- * warnings: string[]
37
- * }>}
38
- */
39
- export async function buildVueProject(projectPath, options = {}) {
40
- const { silent = false } = options;
41
- const resolvedPath = resolve(projectPath);
42
- const warnings = [];
43
-
44
- // Check for package.json
45
- if (!existsSync(join(resolvedPath, 'package.json'))) {
46
- return {
47
- success: false,
48
- output: '',
49
- error: `No package.json found in ${resolvedPath}`,
50
- distPath: null,
51
- warnings
52
- };
53
- }
54
-
55
- // Check for node_modules
56
- if (!existsSync(join(resolvedPath, 'node_modules'))) {
57
- return {
58
- success: false,
59
- output: '',
60
- error: `No node_modules found.\n\nRun the following command first:\n cd "${resolvedPath}"\n npm install`,
61
- distPath: null,
62
- warnings
63
- };
64
- }
65
-
66
- // Acquire build lock to prevent concurrent builds
67
- // Use os.tmpdir() as basePath to avoid permission issues in project directories
68
- const lockContext = createProjectContext(resolvedPath);
69
- const lockResult = acquireLock(LockTypes.BUILD, {
70
- context: lockContext,
71
- operation: `building ${basename(resolvedPath)}`,
72
- basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
73
- });
74
-
75
- // If lock acquisition fails due to permissions, warn but continue
76
- // (locking is nice-to-have, not critical for builds)
77
- let lockAcquired = lockResult.acquired;
78
- if (!lockResult.acquired) {
79
- // Check if it's a permission error vs actual concurrent operation
80
- if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
81
- warnings.push('Could not create build lock (permission issue). Proceeding without lock.');
82
- lockAcquired = false;
83
- } else {
84
- // Actual concurrent operation - fail
85
- return {
86
- success: false,
87
- output: '',
88
- error: lockResult.error,
89
- distPath: null,
90
- warnings
91
- };
92
- }
93
- }
94
-
95
- try {
96
- return await new Promise((resolvePromise) => {
97
- const output = [];
98
- const errorOutput = [];
99
-
100
- // Determine npm command based on platform
101
- const isWindows = process.platform === 'win32';
102
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
103
-
104
- const child = spawn(npmCmd, ['run', 'build'], {
105
- cwd: resolvedPath,
106
- shell: isWindows, // Windows requires shell: true for .cmd files
107
- env: { ...process.env, FORCE_COLOR: '1' }
108
- });
109
-
110
- child.stdout.on('data', (data) => {
111
- const text = data.toString();
112
- output.push(text);
113
- if (!silent) {
114
- process.stdout.write(text);
115
- }
116
- });
117
-
118
- child.stderr.on('data', (data) => {
119
- const text = data.toString();
120
- errorOutput.push(text);
121
- if (!silent) {
122
- process.stderr.write(text);
123
- }
124
- });
125
-
126
- child.on('close', (code) => {
127
- const fullOutput = output.join('');
128
- const fullError = errorOutput.join('');
129
-
130
- if (code !== 0) {
131
- resolvePromise({
132
- success: false,
133
- output: fullOutput,
134
- error: fullError || `Build process exited with code ${code}`,
135
- distPath: null,
136
- warnings
137
- });
138
- return;
139
- }
140
-
141
- // Find dist directory (Vue typically outputs to 'dist')
142
- const possibleDistPaths = ['dist', 'build', 'output'];
143
- let distPath = null;
144
-
145
- for (const distDir of possibleDistPaths) {
146
- const fullPath = join(resolvedPath, distDir);
147
- if (existsSync(fullPath)) {
148
- distPath = fullPath;
149
- break;
150
- }
151
- }
152
-
153
- if (!distPath) {
154
- resolvePromise({
155
- success: false,
156
- output: fullOutput,
157
- error: 'Build completed but no dist/build directory found.\n\n' +
158
- 'Expected one of: dist/, build/, or output/',
159
- distPath: null,
160
- warnings
161
- });
162
- return;
163
- }
164
-
165
- // Check dist folder size
166
- const distSize = getDirectorySize(distPath);
167
- if (distSize > MAX_DIST_SIZE_WARNING) {
168
- const sizeMB = Math.round(distSize / (1024 * 1024));
169
- warnings.push(`Large build output (${sizeMB}MB). This may take longer to stage and publish.`);
170
- }
171
-
172
- resolvePromise({
173
- success: true,
174
- output: fullOutput,
175
- error: null,
176
- distPath,
177
- warnings
178
- });
179
- });
180
-
181
- child.on('error', (err) => {
182
- resolvePromise({
183
- success: false,
184
- output: output.join(''),
185
- error: `Failed to start build process: ${err.message}`,
186
- distPath: null,
187
- warnings
188
- });
189
- });
190
- });
191
- } finally {
192
- // Release the lock only if we acquired it
193
- if (lockAcquired) {
194
- releaseLock(LockTypes.BUILD, {
195
- context: lockContext,
196
- basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
197
- });
198
- }
199
- }
200
- }
201
-
202
- /**
203
- * Calculate total size of a directory recursively.
204
- *
205
- * @param {string} dirPath - Directory path
206
- * @returns {number} - Total size in bytes
207
- */
208
- function getDirectorySize(dirPath) {
209
- let totalSize = 0;
210
-
211
- try {
212
- const items = readdirSync(dirPath, { withFileTypes: true });
213
- for (const item of items) {
214
- const itemPath = join(dirPath, item.name);
215
- if (item.isDirectory()) {
216
- totalSize += getDirectorySize(itemPath);
217
- } else {
218
- try {
219
- const stats = statSync(itemPath);
220
- totalSize += stats.size;
221
- } catch {
222
- // Skip files we can't stat
223
- }
224
- }
225
- }
226
- } catch {
227
- // Return what we have so far
228
- }
229
-
230
- return totalSize;
231
- }
232
-
233
- /**
234
- * Stage build output to a Magentrix workspace's iris-apps directory.
235
- *
236
- * @param {string} distPath - Path to the build output (dist directory)
237
- * @param {string} slug - The app slug (folder name)
238
- * @param {string} workspacePath - Path to the Magentrix workspace (defaults to CWD)
239
- * @returns {{
240
- * success: boolean,
241
- * stagedPath: string | null,
242
- * error: string | null,
243
- * fileCount: number,
244
- * warnings: string[]
245
- * }}
246
- */
247
- export function stageToWorkspace(distPath, slug, workspacePath = process.cwd()) {
248
- const resolvedDistPath = resolve(distPath);
249
- const irisAppsDir = join(workspacePath, EXPORT_ROOT, IRIS_APPS_DIR);
250
- const targetDir = join(irisAppsDir, slug);
251
- const warnings = [];
252
-
253
- // Validate dist path exists
254
- if (!existsSync(resolvedDistPath)) {
255
- return {
256
- success: false,
257
- stagedPath: null,
258
- error: `Dist path does not exist: ${resolvedDistPath}\n\n` +
259
- `The source folder may have been deleted or moved.`,
260
- fileCount: 0,
261
- warnings
262
- };
263
- }
264
-
265
- // Validate build output
266
- const validation = validateIrisBuild(resolvedDistPath);
267
- if (!validation.valid) {
268
- return {
269
- success: false,
270
- stagedPath: null,
271
- error: `Invalid build output:\n${validation.errors.join('\n')}`,
272
- fileCount: 0,
273
- warnings
274
- };
275
- }
276
-
277
- // Acquire staging lock (use user home for consistent lock location)
278
- const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
279
- const lockResult = acquireLock(LockTypes.STAGE, {
280
- context: slug,
281
- operation: `staging ${slug}`,
282
- basePath: lockBasePath
283
- });
284
-
285
- // If lock acquisition fails due to permissions, warn but continue
286
- let lockAcquired = lockResult.acquired;
287
- if (!lockResult.acquired) {
288
- if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
289
- warnings.push('Could not create staging lock (permission issue). Proceeding without lock.');
290
- lockAcquired = false;
291
- } else {
292
- return {
293
- success: false,
294
- stagedPath: null,
295
- error: lockResult.error,
296
- fileCount: 0,
297
- warnings
298
- };
299
- }
300
- }
301
-
302
- try {
303
- // Ensure iris-apps directory exists
304
- try {
305
- if (!existsSync(irisAppsDir)) {
306
- mkdirSync(irisAppsDir, { recursive: true });
307
- }
308
- } catch (err) {
309
- const errorType = detectErrorType(err);
310
- if (errorType === ErrorTypes.PERMISSION) {
311
- return {
312
- success: false,
313
- stagedPath: null,
314
- error: formatPermissionError({
315
- operation: 'create directory',
316
- path: irisAppsDir
317
- }),
318
- fileCount: 0,
319
- warnings
320
- };
321
- }
322
- return {
323
- success: false,
324
- stagedPath: null,
325
- error: `Failed to create iris-apps directory: ${err.message}`,
326
- fileCount: 0,
327
- warnings
328
- };
329
- }
330
-
331
- // Remove existing app directory if it exists
332
- if (existsSync(targetDir)) {
333
- try {
334
- rmSync(targetDir, { recursive: true, force: true });
335
- } catch (err) {
336
- const errorType = detectErrorType(err);
337
- if (errorType === ErrorTypes.PERMISSION) {
338
- return {
339
- success: false,
340
- stagedPath: null,
341
- error: formatPermissionError({
342
- operation: 'remove existing directory',
343
- path: targetDir
344
- }),
345
- fileCount: 0,
346
- warnings
347
- };
348
- }
349
- if (errorType === ErrorTypes.FILE_LOCKED) {
350
- return {
351
- success: false,
352
- stagedPath: null,
353
- error: formatFileLockError({ path: targetDir }),
354
- fileCount: 0,
355
- warnings
356
- };
357
- }
358
- return {
359
- success: false,
360
- stagedPath: null,
361
- error: `Failed to remove existing app directory: ${err.message}`,
362
- fileCount: 0,
363
- warnings
364
- };
365
- }
366
- }
367
-
368
- // Copy dist contents to target, excluding unnecessary files
369
- try {
370
- cpSync(resolvedDistPath, targetDir, {
371
- recursive: true,
372
- filter: (src) => {
373
- // Use basename for reliable cross-platform filename extraction
374
- const filename = basename(src);
375
- // Only exclude specific files, not directories
376
- if (EXCLUDED_FILES.has(filename)) {
377
- return false;
378
- }
379
- return true;
380
- }
381
- });
382
- } catch (err) {
383
- const errorType = detectErrorType(err);
384
- if (errorType === ErrorTypes.PERMISSION) {
385
- return {
386
- success: false,
387
- stagedPath: null,
388
- error: formatPermissionError({
389
- operation: 'copy files',
390
- path: targetDir
391
- }),
392
- fileCount: 0,
393
- warnings
394
- };
395
- }
396
- if (errorType === ErrorTypes.DISK_FULL) {
397
- return {
398
- success: false,
399
- stagedPath: null,
400
- error: formatDiskFullError({
401
- operation: 'copy build files',
402
- path: targetDir
403
- }),
404
- fileCount: 0,
405
- warnings
406
- };
407
- }
408
- return {
409
- success: false,
410
- stagedPath: null,
411
- error: `Failed to copy build output: ${err.message}`,
412
- fileCount: 0,
413
- warnings
414
- };
415
- }
416
-
417
- // Count files copied
418
- const fileCount = countFiles(targetDir);
419
-
420
- // Update last build timestamp for linked project (ignore errors)
421
- const updateResult = updateLastBuild(slug);
422
- if (!updateResult.success && updateResult.error) {
423
- warnings.push(`Could not update last build timestamp: ${updateResult.error}`);
424
- }
425
-
426
- return {
427
- success: true,
428
- stagedPath: targetDir,
429
- error: null,
430
- fileCount,
431
- warnings
432
- };
433
- } finally {
434
- // Release the lock only if we acquired it
435
- if (lockAcquired) {
436
- releaseLock(LockTypes.STAGE, { context: slug, basePath: lockBasePath });
437
- }
438
- }
439
- }
440
-
441
- /**
442
- * Count files recursively in a directory.
443
- *
444
- * @param {string} dir - Directory to count files in
445
- * @returns {number} - Number of files
446
- */
447
- function countFiles(dir) {
448
- if (!existsSync(dir)) return 0;
449
-
450
- let count = 0;
451
- const items = readdirSync(dir, { withFileTypes: true });
452
-
453
- for (const item of items) {
454
- if (item.isDirectory()) {
455
- count += countFiles(join(dir, item.name));
456
- } else {
457
- count++;
458
- }
459
- }
460
-
461
- return count;
462
- }
463
-
464
- /**
465
- * Find an existing dist directory in a Vue project.
466
- *
467
- * @param {string} projectPath - Path to the Vue project
468
- * @returns {string | null} - Path to dist directory or null if not found
469
- */
470
- export function findDistDirectory(projectPath) {
471
- const possibleDistPaths = ['dist', 'build', 'output'];
472
-
473
- for (const distDir of possibleDistPaths) {
474
- const fullPath = join(projectPath, distDir);
475
- if (existsSync(fullPath)) {
476
- return fullPath;
477
- }
478
- }
479
-
480
- return null;
481
- }
482
-
483
- /**
484
- * Format build error for display.
485
- *
486
- * @param {string} projectPath - Path to the Vue project
487
- * @param {string} errorOutput - Error output from build
488
- * @returns {string} - Formatted error message
489
- */
490
- export function formatBuildError(projectPath, errorOutput) {
491
- return `Build Failed
492
- ────────────────────────────────────────────────────
493
-
494
- The Vue.js build process failed. This is a project issue, not a CLI issue.
495
-
496
- Error output:
497
- ${errorOutput.split('\n').map(line => ` ${line}`).join('\n')}
498
-
499
- To fix this:
500
- 1. Run 'npm run build' manually in your Vue project:
501
- cd ${projectPath}
502
- npm run build
503
-
504
- 2. Fix any compilation errors shown above
505
-
506
- 3. Run 'magentrix vue-run-build' again
507
-
508
- Alternatively, use --skip-build to stage an existing dist/ folder.`;
509
- }
510
-
511
- /**
512
- * Format validation error for display.
513
- *
514
- * @param {string} distPath - Path to the dist directory
515
- * @param {string[]} errors - Validation errors
516
- * @returns {string} - Formatted error message
517
- */
518
- export function formatValidationError(distPath, errors) {
519
- return `Invalid Build Output
520
- ────────────────────────────────────────────────────
521
-
522
- The build output at ${distPath} is missing required files.
523
-
524
- Missing:
525
- ${errors.map(e => ` ✗ ${e}`).join('\n')}
526
-
527
- Required files for an Iris app:
528
- - remoteEntry.js (Module Federation entry point)
529
- - assets/hostInit*.js (Host initialization script)
530
- - assets/main*.js (Main entry point)
531
- - assets/index*.js (Index entry)
532
-
533
- This usually means:
534
- 1. The project is not configured as a Module Federation remote
535
- 2. The build did not complete successfully
536
- 3. The dist directory contains an old or incorrect build
537
-
538
- Try rebuilding the project:
539
- cd ${distPath.replace('/dist', '')}
540
- npm run build`;
541
- }
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from 'node:fs';
3
+ import { join, resolve, basename } from 'node:path';
4
+ import { validateIrisBuild } from './validator.js';
5
+ import { updateLastBuild } from './linker.js';
6
+ import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
7
+ import { detectErrorType, ErrorTypes, formatPermissionError, formatDiskFullError, formatFileLockError } from './errors.js';
8
+ import { acquireLock, releaseLock, LockTypes, createProjectContext } from './lock.js';
9
+
10
+ // Files to exclude when staging Iris apps (not needed on server)
11
+ const EXCLUDED_FILES = new Set([
12
+ 'index.html',
13
+ 'favicon.ico',
14
+ '.DS_Store',
15
+ 'Thumbs.db',
16
+ '.gitignore',
17
+ '.gitkeep'
18
+ ]);
19
+
20
+ /**
21
+ * Maximum dist folder size before warning (500MB)
22
+ */
23
+ const MAX_DIST_SIZE_WARNING = 500 * 1024 * 1024;
24
+
25
+ /**
26
+ * Build a Vue project using npm run build.
27
+ *
28
+ * @param {string} projectPath - Path to the Vue project
29
+ * @param {Object} options - Build options
30
+ * @param {boolean} options.silent - Suppress build output
31
+ * @returns {Promise<{
32
+ * success: boolean,
33
+ * output: string,
34
+ * error: string | null,
35
+ * distPath: string | null,
36
+ * warnings: string[]
37
+ * }>}
38
+ */
39
+ export async function buildVueProject(projectPath, options = {}) {
40
+ const { silent = false } = options;
41
+ const resolvedPath = resolve(projectPath);
42
+ const warnings = [];
43
+
44
+ // Check for package.json
45
+ if (!existsSync(join(resolvedPath, 'package.json'))) {
46
+ return {
47
+ success: false,
48
+ output: '',
49
+ error: `No package.json found in ${resolvedPath}`,
50
+ distPath: null,
51
+ warnings
52
+ };
53
+ }
54
+
55
+ // Check for node_modules
56
+ if (!existsSync(join(resolvedPath, 'node_modules'))) {
57
+ return {
58
+ success: false,
59
+ output: '',
60
+ error: `No node_modules found.\n\nRun the following command first:\n cd "${resolvedPath}"\n npm install`,
61
+ distPath: null,
62
+ warnings
63
+ };
64
+ }
65
+
66
+ // Acquire build lock to prevent concurrent builds
67
+ // Use os.tmpdir() as basePath to avoid permission issues in project directories
68
+ const lockContext = createProjectContext(resolvedPath);
69
+ const lockResult = acquireLock(LockTypes.BUILD, {
70
+ context: lockContext,
71
+ operation: `building ${basename(resolvedPath)}`,
72
+ basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
73
+ });
74
+
75
+ // If lock acquisition fails due to permissions, warn but continue
76
+ // (locking is nice-to-have, not critical for builds)
77
+ let lockAcquired = lockResult.acquired;
78
+ if (!lockResult.acquired) {
79
+ // Check if it's a permission error vs actual concurrent operation
80
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
81
+ warnings.push('Could not create build lock (permission issue). Proceeding without lock.');
82
+ lockAcquired = false;
83
+ } else {
84
+ // Actual concurrent operation - fail
85
+ return {
86
+ success: false,
87
+ output: '',
88
+ error: lockResult.error,
89
+ distPath: null,
90
+ warnings
91
+ };
92
+ }
93
+ }
94
+
95
+ try {
96
+ return await new Promise((resolvePromise) => {
97
+ const output = [];
98
+ const errorOutput = [];
99
+
100
+ // Determine npm command based on platform
101
+ const isWindows = process.platform === 'win32';
102
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
103
+
104
+ const child = spawn(npmCmd, ['run', 'build'], {
105
+ cwd: resolvedPath,
106
+ shell: isWindows, // Windows requires shell: true for .cmd files
107
+ env: { ...process.env, FORCE_COLOR: '1' }
108
+ });
109
+
110
+ child.stdout.on('data', (data) => {
111
+ const text = data.toString();
112
+ output.push(text);
113
+ if (!silent) {
114
+ process.stdout.write(text);
115
+ }
116
+ });
117
+
118
+ child.stderr.on('data', (data) => {
119
+ const text = data.toString();
120
+ errorOutput.push(text);
121
+ if (!silent) {
122
+ process.stderr.write(text);
123
+ }
124
+ });
125
+
126
+ child.on('close', (code) => {
127
+ const fullOutput = output.join('');
128
+ const fullError = errorOutput.join('');
129
+
130
+ if (code !== 0) {
131
+ resolvePromise({
132
+ success: false,
133
+ output: fullOutput,
134
+ error: fullError || `Build process exited with code ${code}`,
135
+ distPath: null,
136
+ warnings
137
+ });
138
+ return;
139
+ }
140
+
141
+ // Find dist directory (Vue typically outputs to 'dist')
142
+ const possibleDistPaths = ['dist', 'build', 'output'];
143
+ let distPath = null;
144
+
145
+ for (const distDir of possibleDistPaths) {
146
+ const fullPath = join(resolvedPath, distDir);
147
+ if (existsSync(fullPath)) {
148
+ distPath = fullPath;
149
+ break;
150
+ }
151
+ }
152
+
153
+ if (!distPath) {
154
+ resolvePromise({
155
+ success: false,
156
+ output: fullOutput,
157
+ error: 'Build completed but no dist/build directory found.\n\n' +
158
+ 'Expected one of: dist/, build/, or output/',
159
+ distPath: null,
160
+ warnings
161
+ });
162
+ return;
163
+ }
164
+
165
+ // Check dist folder size
166
+ const distSize = getDirectorySize(distPath);
167
+ if (distSize > MAX_DIST_SIZE_WARNING) {
168
+ const sizeMB = Math.round(distSize / (1024 * 1024));
169
+ warnings.push(`Large build output (${sizeMB}MB). This may take longer to stage and publish.`);
170
+ }
171
+
172
+ resolvePromise({
173
+ success: true,
174
+ output: fullOutput,
175
+ error: null,
176
+ distPath,
177
+ warnings
178
+ });
179
+ });
180
+
181
+ child.on('error', (err) => {
182
+ resolvePromise({
183
+ success: false,
184
+ output: output.join(''),
185
+ error: `Failed to start build process: ${err.message}`,
186
+ distPath: null,
187
+ warnings
188
+ });
189
+ });
190
+ });
191
+ } finally {
192
+ // Release the lock only if we acquired it
193
+ if (lockAcquired) {
194
+ releaseLock(LockTypes.BUILD, {
195
+ context: lockContext,
196
+ basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
197
+ });
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Calculate total size of a directory recursively.
204
+ *
205
+ * @param {string} dirPath - Directory path
206
+ * @returns {number} - Total size in bytes
207
+ */
208
+ function getDirectorySize(dirPath) {
209
+ let totalSize = 0;
210
+
211
+ try {
212
+ const items = readdirSync(dirPath, { withFileTypes: true });
213
+ for (const item of items) {
214
+ const itemPath = join(dirPath, item.name);
215
+ if (item.isDirectory()) {
216
+ totalSize += getDirectorySize(itemPath);
217
+ } else {
218
+ try {
219
+ const stats = statSync(itemPath);
220
+ totalSize += stats.size;
221
+ } catch {
222
+ // Skip files we can't stat
223
+ }
224
+ }
225
+ }
226
+ } catch {
227
+ // Return what we have so far
228
+ }
229
+
230
+ return totalSize;
231
+ }
232
+
233
+ /**
234
+ * Stage build output to a Magentrix workspace's iris-apps directory.
235
+ *
236
+ * @param {string} distPath - Path to the build output (dist directory)
237
+ * @param {string} slug - The app slug (folder name)
238
+ * @param {string} workspacePath - Path to the Magentrix workspace (defaults to CWD)
239
+ * @returns {{
240
+ * success: boolean,
241
+ * stagedPath: string | null,
242
+ * error: string | null,
243
+ * fileCount: number,
244
+ * warnings: string[]
245
+ * }}
246
+ */
247
+ export function stageToWorkspace(distPath, slug, workspacePath = process.cwd()) {
248
+ const resolvedDistPath = resolve(distPath);
249
+ const irisAppsDir = join(workspacePath, EXPORT_ROOT, IRIS_APPS_DIR);
250
+ const targetDir = join(irisAppsDir, slug);
251
+ const warnings = [];
252
+
253
+ // Validate dist path exists
254
+ if (!existsSync(resolvedDistPath)) {
255
+ return {
256
+ success: false,
257
+ stagedPath: null,
258
+ error: `Dist path does not exist: ${resolvedDistPath}\n\n` +
259
+ `The source folder may have been deleted or moved.`,
260
+ fileCount: 0,
261
+ warnings
262
+ };
263
+ }
264
+
265
+ // Validate build output
266
+ const validation = validateIrisBuild(resolvedDistPath);
267
+ if (!validation.valid) {
268
+ return {
269
+ success: false,
270
+ stagedPath: null,
271
+ error: `Invalid build output:\n${validation.errors.join('\n')}`,
272
+ fileCount: 0,
273
+ warnings
274
+ };
275
+ }
276
+
277
+ // Acquire staging lock (use user home for consistent lock location)
278
+ const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
279
+ const lockResult = acquireLock(LockTypes.STAGE, {
280
+ context: slug,
281
+ operation: `staging ${slug}`,
282
+ basePath: lockBasePath
283
+ });
284
+
285
+ // If lock acquisition fails due to permissions, warn but continue
286
+ let lockAcquired = lockResult.acquired;
287
+ if (!lockResult.acquired) {
288
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
289
+ warnings.push('Could not create staging lock (permission issue). Proceeding without lock.');
290
+ lockAcquired = false;
291
+ } else {
292
+ return {
293
+ success: false,
294
+ stagedPath: null,
295
+ error: lockResult.error,
296
+ fileCount: 0,
297
+ warnings
298
+ };
299
+ }
300
+ }
301
+
302
+ try {
303
+ // Ensure iris-apps directory exists
304
+ try {
305
+ if (!existsSync(irisAppsDir)) {
306
+ mkdirSync(irisAppsDir, { recursive: true });
307
+ }
308
+ } catch (err) {
309
+ const errorType = detectErrorType(err);
310
+ if (errorType === ErrorTypes.PERMISSION) {
311
+ return {
312
+ success: false,
313
+ stagedPath: null,
314
+ error: formatPermissionError({
315
+ operation: 'create directory',
316
+ path: irisAppsDir
317
+ }),
318
+ fileCount: 0,
319
+ warnings
320
+ };
321
+ }
322
+ return {
323
+ success: false,
324
+ stagedPath: null,
325
+ error: `Failed to create iris-apps directory: ${err.message}`,
326
+ fileCount: 0,
327
+ warnings
328
+ };
329
+ }
330
+
331
+ // Remove existing app directory if it exists
332
+ if (existsSync(targetDir)) {
333
+ try {
334
+ rmSync(targetDir, { recursive: true, force: true });
335
+ } catch (err) {
336
+ const errorType = detectErrorType(err);
337
+ if (errorType === ErrorTypes.PERMISSION) {
338
+ return {
339
+ success: false,
340
+ stagedPath: null,
341
+ error: formatPermissionError({
342
+ operation: 'remove existing directory',
343
+ path: targetDir
344
+ }),
345
+ fileCount: 0,
346
+ warnings
347
+ };
348
+ }
349
+ if (errorType === ErrorTypes.FILE_LOCKED) {
350
+ return {
351
+ success: false,
352
+ stagedPath: null,
353
+ error: formatFileLockError({ path: targetDir }),
354
+ fileCount: 0,
355
+ warnings
356
+ };
357
+ }
358
+ return {
359
+ success: false,
360
+ stagedPath: null,
361
+ error: `Failed to remove existing app directory: ${err.message}`,
362
+ fileCount: 0,
363
+ warnings
364
+ };
365
+ }
366
+ }
367
+
368
+ // Copy dist contents to target, excluding unnecessary files
369
+ try {
370
+ cpSync(resolvedDistPath, targetDir, {
371
+ recursive: true,
372
+ filter: (src) => {
373
+ // Use basename for reliable cross-platform filename extraction
374
+ const filename = basename(src);
375
+ // Only exclude specific files, not directories
376
+ if (EXCLUDED_FILES.has(filename)) {
377
+ return false;
378
+ }
379
+ return true;
380
+ }
381
+ });
382
+ } catch (err) {
383
+ const errorType = detectErrorType(err);
384
+ if (errorType === ErrorTypes.PERMISSION) {
385
+ return {
386
+ success: false,
387
+ stagedPath: null,
388
+ error: formatPermissionError({
389
+ operation: 'copy files',
390
+ path: targetDir
391
+ }),
392
+ fileCount: 0,
393
+ warnings
394
+ };
395
+ }
396
+ if (errorType === ErrorTypes.DISK_FULL) {
397
+ return {
398
+ success: false,
399
+ stagedPath: null,
400
+ error: formatDiskFullError({
401
+ operation: 'copy build files',
402
+ path: targetDir
403
+ }),
404
+ fileCount: 0,
405
+ warnings
406
+ };
407
+ }
408
+ return {
409
+ success: false,
410
+ stagedPath: null,
411
+ error: `Failed to copy build output: ${err.message}`,
412
+ fileCount: 0,
413
+ warnings
414
+ };
415
+ }
416
+
417
+ // Count files copied
418
+ const fileCount = countFiles(targetDir);
419
+
420
+ // Update last build timestamp for linked project (ignore errors)
421
+ const updateResult = updateLastBuild(slug);
422
+ if (!updateResult.success && updateResult.error) {
423
+ warnings.push(`Could not update last build timestamp: ${updateResult.error}`);
424
+ }
425
+
426
+ return {
427
+ success: true,
428
+ stagedPath: targetDir,
429
+ error: null,
430
+ fileCount,
431
+ warnings
432
+ };
433
+ } finally {
434
+ // Release the lock only if we acquired it
435
+ if (lockAcquired) {
436
+ releaseLock(LockTypes.STAGE, { context: slug, basePath: lockBasePath });
437
+ }
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Count files recursively in a directory.
443
+ *
444
+ * @param {string} dir - Directory to count files in
445
+ * @returns {number} - Number of files
446
+ */
447
+ function countFiles(dir) {
448
+ if (!existsSync(dir)) return 0;
449
+
450
+ let count = 0;
451
+ const items = readdirSync(dir, { withFileTypes: true });
452
+
453
+ for (const item of items) {
454
+ if (item.isDirectory()) {
455
+ count += countFiles(join(dir, item.name));
456
+ } else {
457
+ count++;
458
+ }
459
+ }
460
+
461
+ return count;
462
+ }
463
+
464
+ /**
465
+ * Find an existing dist directory in a Vue project.
466
+ *
467
+ * @param {string} projectPath - Path to the Vue project
468
+ * @returns {string | null} - Path to dist directory or null if not found
469
+ */
470
+ export function findDistDirectory(projectPath) {
471
+ const possibleDistPaths = ['dist', 'build', 'output'];
472
+
473
+ for (const distDir of possibleDistPaths) {
474
+ const fullPath = join(projectPath, distDir);
475
+ if (existsSync(fullPath)) {
476
+ return fullPath;
477
+ }
478
+ }
479
+
480
+ return null;
481
+ }
482
+
483
+ /**
484
+ * Format build error for display.
485
+ *
486
+ * @param {string} projectPath - Path to the Vue project
487
+ * @param {string} errorOutput - Error output from build
488
+ * @returns {string} - Formatted error message
489
+ */
490
+ export function formatBuildError(projectPath, errorOutput) {
491
+ return `Build Failed
492
+ ────────────────────────────────────────────────────
493
+
494
+ The Vue.js build process failed. This is a project issue, not a CLI issue.
495
+
496
+ Error output:
497
+ ${errorOutput.split('\n').map(line => ` ${line}`).join('\n')}
498
+
499
+ To fix this:
500
+ 1. Run 'npm run build' manually in your Vue project:
501
+ cd ${projectPath}
502
+ npm run build
503
+
504
+ 2. Fix any compilation errors shown above
505
+
506
+ 3. Run 'magentrix vue-run-build' again
507
+
508
+ Alternatively, use --skip-build to stage an existing dist/ folder.`;
509
+ }
510
+
511
+ /**
512
+ * Format validation error for display.
513
+ *
514
+ * @param {string} distPath - Path to the dist directory
515
+ * @param {string[]} errors - Validation errors
516
+ * @returns {string} - Formatted error message
517
+ */
518
+ export function formatValidationError(distPath, errors) {
519
+ return `Invalid Build Output
520
+ ────────────────────────────────────────────────────
521
+
522
+ The build output at ${distPath} is missing required files.
523
+
524
+ Missing:
525
+ ${errors.map(e => ` ✗ ${e}`).join('\n')}
526
+
527
+ Required files for an Iris app:
528
+ - remoteEntry.js (Module Federation entry point)
529
+ - assets/hostInit*.js (Host initialization script)
530
+ - assets/main*.js (Main entry point)
531
+ - assets/index*.js (Index entry)
532
+
533
+ This usually means:
534
+ 1. The project is not configured as a Module Federation remote
535
+ 2. The build did not complete successfully
536
+ 3. The dist directory contains an old or incorrect build
537
+
538
+ Try rebuilding the project:
539
+ cd ${distPath.replace('/dist', '')}
540
+ npm run build`;
541
+ }