@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,391 +1,391 @@
1
- import chalk from 'chalk';
2
- import { select, input } from '@inquirer/prompts';
3
- import { spawn } from 'node:child_process';
4
- import { existsSync } from 'node:fs';
5
- import { resolve, basename, join } from 'node:path';
6
- import {
7
- readVueConfig,
8
- formatMissingConfigError,
9
- formatConfigErrors,
10
- injectAssets,
11
- getInjectionTarget
12
- } from '../../utils/iris/config-reader.js';
13
- import { getIrisAssets } from '../../utils/magentrix/api/iris.js';
14
- import { getAccessToken } from '../../utils/magentrix/api/auth.js';
15
- import {
16
- getLinkedProjectsWithStatus,
17
- buildProjectChoices
18
- } from '../../utils/iris/linker.js';
19
- import {
20
- acquireLock,
21
- releaseLock,
22
- LockTypes,
23
- createProjectContext
24
- } from '../../utils/iris/lock.js';
25
- import {
26
- formatNetworkError,
27
- formatError
28
- } from '../../utils/iris/errors.js';
29
-
30
- /**
31
- * Timeout for fetching assets (30 seconds).
32
- */
33
- const ASSET_FETCH_TIMEOUT = 30000;
34
-
35
- /**
36
- * vue-run-dev command - Start Vue dev server with platform assets injected.
37
- *
38
- * Uses credentials from .env.development (VITE_REFRESH_TOKEN, VITE_SITE_URL).
39
- * Assets are updated in .env.development (VITE_ASSETS) and kept between runs.
40
- *
41
- * Options:
42
- * --path <dir> Specify Vue project path
43
- * --no-inject Skip asset injection, just run dev server
44
- */
45
- export const irisDev = async (options = {}) => {
46
- process.stdout.write('\x1Bc'); // Clear console
47
-
48
- const { path: pathOption, inject = true } = options;
49
-
50
- // Determine which project to use
51
- let projectPath = pathOption;
52
- let vueConfig = null;
53
-
54
- if (projectPath) {
55
- projectPath = resolve(projectPath);
56
-
57
- if (!existsSync(projectPath)) {
58
- console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
59
- return;
60
- }
61
-
62
- vueConfig = readVueConfig(projectPath);
63
- } else {
64
- // Try current directory first
65
- const cwdConfig = readVueConfig(process.cwd());
66
- if (cwdConfig.found && cwdConfig.errors.length === 0) {
67
- projectPath = process.cwd();
68
- vueConfig = cwdConfig;
69
- console.log(chalk.gray(`Using current directory: ${projectPath}`));
70
- } else {
71
- // Prompt user to select a project
72
- const result = await selectProject();
73
- if (!result) return;
74
-
75
- projectPath = result.path;
76
- vueConfig = result.config;
77
- }
78
- }
79
-
80
- // Validate Vue config
81
- if (!vueConfig.found) {
82
- console.log(chalk.red(formatMissingConfigError(projectPath)));
83
- return;
84
- }
85
-
86
- if (vueConfig.errors.length > 0) {
87
- console.log(chalk.red(formatConfigErrors(vueConfig)));
88
- return;
89
- }
90
-
91
- const { slug, appName, siteUrl } = vueConfig;
92
-
93
- console.log(chalk.blue('\nIris Development Server'));
94
- console.log(chalk.gray('─'.repeat(48)));
95
- console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
96
- console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
97
- if (siteUrl) {
98
- console.log(chalk.white(` Site: ${chalk.gray(siteUrl)}`));
99
- }
100
- console.log();
101
-
102
- // Inject assets if enabled
103
- if (inject && siteUrl) {
104
- // Check if we have the refresh token for authentication
105
- if (!vueConfig.refreshToken) {
106
- console.log(chalk.yellow('Warning: VITE_REFRESH_TOKEN not set in .env.development'));
107
- console.log(chalk.gray('Asset injection requires authentication. Continuing without assets.'));
108
- } else {
109
- console.log(chalk.blue('Fetching platform assets...'));
110
-
111
- try {
112
- // Get access token using the refresh token from .env.development
113
- // Use Promise.race to implement timeout
114
- const tokenPromise = getAccessToken(vueConfig.refreshToken, siteUrl);
115
- const timeoutPromise = new Promise((_, reject) =>
116
- setTimeout(() => reject(new Error('Request timed out')), ASSET_FETCH_TIMEOUT)
117
- );
118
-
119
- const tokenData = await Promise.race([tokenPromise, timeoutPromise]);
120
-
121
- // Fetch assets with timeout
122
- const assetsPromise = getIrisAssets(siteUrl, tokenData.token);
123
- const assetsResult = await Promise.race([assetsPromise, timeoutPromise]);
124
-
125
- if (assetsResult.success && assetsResult.assets?.length > 0) {
126
- console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
127
-
128
- // Determine which file will be modified
129
- const { targetFile } = getInjectionTarget(projectPath);
130
-
131
- if (!targetFile) {
132
- console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
133
- console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
134
- } else {
135
- // Inject assets (no backup needed - we keep the changes)
136
- console.log(chalk.blue('Updating assets in .env.development...'));
137
- const injectResult = injectAssets(projectPath, assetsResult.assets);
138
-
139
- if (injectResult.success) {
140
- console.log(chalk.green(`\u2713 Assets updated in ${injectResult.targetName}`));
141
- } else {
142
- // Show specific error if available
143
- if (injectResult.error) {
144
- console.log(chalk.yellow(`Warning: Could not inject assets: ${injectResult.error}`));
145
- } else {
146
- console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
147
- }
148
- }
149
- }
150
- } else if (assetsResult.error) {
151
- console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
152
- console.log(chalk.gray('Continuing without asset injection.'));
153
- } else {
154
- console.log(chalk.yellow('No platform assets found.'));
155
- }
156
- } catch (err) {
157
- // Provide more helpful error messages based on error type
158
- if (err.message === 'Request timed out') {
159
- console.log(chalk.yellow('Warning: Asset fetch timed out.'));
160
- console.log(chalk.gray('Check your network connection or try again later.'));
161
- } else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
162
- console.log(chalk.yellow(formatNetworkError({
163
- operation: 'fetch assets',
164
- url: siteUrl,
165
- error: err
166
- })));
167
- } else if (err.message?.includes('401') || err.message?.includes('Unauthorized')) {
168
- console.log(chalk.yellow('Warning: Authentication failed.'));
169
- console.log(chalk.gray('Your VITE_REFRESH_TOKEN may be invalid or expired.'));
170
- console.log(chalk.gray('Get a new API key from your Magentrix platform.'));
171
- } else {
172
- console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
173
- }
174
- console.log(chalk.gray('Continuing without asset injection.'));
175
- }
176
- }
177
- } else if (!inject) {
178
- console.log(chalk.gray('Skipping asset injection (--no-inject)'));
179
- } else if (!siteUrl) {
180
- console.log(chalk.yellow('Warning: No siteUrl found. Cannot fetch platform assets.'));
181
- console.log(chalk.gray('Set VITE_SITE_URL in .env.development'));
182
- }
183
-
184
- // Start dev server
185
- console.log();
186
- console.log(chalk.blue('Starting development server...'));
187
- console.log(chalk.gray('Press Ctrl+C to stop'));
188
- console.log();
189
-
190
- await runDevServer(projectPath);
191
- };
192
-
193
- /**
194
- * Run the Vue development server.
195
- *
196
- * @param {string} projectPath - Path to the Vue project
197
- * @returns {Promise<number>} - Exit code
198
- */
199
- async function runDevServer(projectPath) {
200
- // Acquire dev server lock to prevent multiple instances
201
- // Use user home for consistent lock location across projects
202
- const lockContext = createProjectContext(projectPath);
203
- const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
204
- const lockResult = acquireLock(LockTypes.DEV_SERVER, {
205
- context: lockContext,
206
- operation: `dev server for ${basename(projectPath)}`,
207
- basePath: lockBasePath
208
- });
209
-
210
- // Track if lock was acquired (permission errors are non-fatal)
211
- let lockAcquired = lockResult.acquired;
212
- if (!lockResult.acquired) {
213
- if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
214
- console.log(chalk.yellow('Warning: Could not create dev server lock (permission issue). Proceeding without lock.'));
215
- lockAcquired = false;
216
- } else {
217
- console.log(chalk.red('Cannot start dev server:'));
218
- console.log(chalk.yellow(lockResult.error));
219
- return 1;
220
- }
221
- }
222
-
223
- return new Promise((resolvePromise) => {
224
- const isWindows = process.platform === 'win32';
225
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
226
-
227
- const child = spawn(npmCmd, ['run', 'dev'], {
228
- cwd: projectPath,
229
- stdio: 'inherit',
230
- shell: isWindows // Windows requires shell: true for .cmd files
231
- });
232
-
233
- // Cleanup function to release lock
234
- const cleanup = () => {
235
- if (lockAcquired) {
236
- releaseLock(LockTypes.DEV_SERVER, { context: lockContext, basePath: lockBasePath });
237
- }
238
- };
239
-
240
- // Handle process signals - use once() to avoid memory leaks
241
- const handleSignal = (signal) => {
242
- cleanup();
243
- child.kill(signal);
244
- // Give child process a moment to exit, then force exit
245
- setTimeout(() => {
246
- process.exit(0);
247
- }, 500);
248
- };
249
-
250
- const sigintHandler = () => handleSignal('SIGINT');
251
- const sigtermHandler = () => handleSignal('SIGTERM');
252
-
253
- process.once('SIGINT', sigintHandler);
254
- process.once('SIGTERM', sigtermHandler);
255
-
256
- // Remove handlers when child exits to avoid memory leaks
257
- child.on('exit', () => {
258
- process.removeListener('SIGINT', sigintHandler);
259
- process.removeListener('SIGTERM', sigtermHandler);
260
- });
261
-
262
- child.on('close', (code) => {
263
- cleanup();
264
-
265
- // If dev server exited with error, show helpful context
266
- if (code !== 0 && code !== null) {
267
- console.log();
268
- console.log(chalk.bgYellow.black(' External Process Error '));
269
- console.log(chalk.yellow('─'.repeat(48)));
270
- console.log(chalk.white('The Vue dev server exited with an error.'));
271
- console.log(chalk.gray('This is NOT a MagentrixCLI issue.'));
272
- console.log();
273
- console.log(chalk.white('Common causes:'));
274
- console.log(chalk.gray(' • Permission issues in node_modules/'));
275
- console.log(chalk.gray(' • Missing dependencies (run npm install)'));
276
- console.log(chalk.gray(' • Port already in use'));
277
- console.log(chalk.gray(' • Syntax errors in project files'));
278
- console.log();
279
- console.log(chalk.white('If you see EACCES/permission errors, try:'));
280
- if (isWindows) {
281
- console.log(chalk.cyan(' Run terminal as Administrator'));
282
- console.log(chalk.cyan(` rd /s /q "${projectPath}\\node_modules"`));
283
- console.log(chalk.cyan(' npm install'));
284
- } else {
285
- console.log(chalk.cyan(` sudo chown -R $(whoami) "${projectPath}/node_modules"`));
286
- }
287
- console.log(chalk.yellow('─'.repeat(48)));
288
- }
289
-
290
- resolvePromise(code);
291
- });
292
-
293
- child.on('error', (err) => {
294
- cleanup();
295
- console.log(chalk.red(`Failed to start dev server: ${err.message}`));
296
-
297
- // Provide more specific error messages
298
- if (err.code === 'ENOENT') {
299
- console.log();
300
- console.log(chalk.yellow('npm was not found on your system.'));
301
- console.log(chalk.gray('Make sure Node.js is installed and npm is in your PATH.'));
302
- } else if (err.code === 'EACCES' || err.code === 'EPERM') {
303
- console.log();
304
- console.log(chalk.yellow('Permission denied.'));
305
- if (isWindows) {
306
- console.log(chalk.gray('Try running the terminal as Administrator.'));
307
- } else {
308
- console.log(chalk.gray('Check file permissions for the project directory.'));
309
- }
310
- }
311
-
312
- resolvePromise(1);
313
- });
314
- });
315
- }
316
-
317
- /**
318
- * Prompt user to select a project.
319
- */
320
- async function selectProject() {
321
- const projectsWithStatus = getLinkedProjectsWithStatus();
322
-
323
- // Check if CWD is a Vue project (and not already linked)
324
- const cwdConfig = readVueConfig(process.cwd());
325
- const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
326
- const cwdAlreadyLinked = projectsWithStatus.some(p =>
327
- resolve(p.path) === resolve(process.cwd())
328
- );
329
-
330
- // Build choices using the helper
331
- const choices = buildProjectChoices({
332
- includeManual: true,
333
- includeCancel: true,
334
- showInvalid: true
335
- });
336
-
337
- // If CWD is a valid Vue project and not linked, add it at the top
338
- if (inVueProject && !cwdAlreadyLinked) {
339
- choices.unshift({
340
- name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
341
- value: { type: 'cwd', path: process.cwd() }
342
- });
343
- }
344
-
345
- // Check if we have any projects to show
346
- const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
347
-
348
- if (!hasProjects) {
349
- console.log(chalk.yellow('No linked Vue projects found.'));
350
- console.log();
351
- console.log(chalk.gray('To get started:'));
352
- console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
353
- console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-dev --path /path/to/vue-project')}`));
354
- console.log();
355
- }
356
-
357
- const choice = await select({
358
- message: 'Which project do you want to run?',
359
- choices
360
- });
361
-
362
- if (choice.type === 'cancel') {
363
- console.log(chalk.gray('Cancelled.'));
364
- return null;
365
- }
366
-
367
- if (choice.type === 'manual') {
368
- const manualPath = await input({
369
- message: 'Enter the path to your Vue project:',
370
- validate: (value) => {
371
- if (!value.trim()) {
372
- return 'Path is required';
373
- }
374
- const resolved = resolve(value);
375
- if (!existsSync(resolved)) {
376
- return `Path does not exist: ${resolved}`;
377
- }
378
- return true;
379
- }
380
- });
381
-
382
- const config = readVueConfig(resolve(manualPath));
383
- return { path: resolve(manualPath), config };
384
- }
385
-
386
- // Linked project or CWD
387
- const config = readVueConfig(choice.path);
388
- return { path: choice.path, config };
389
- }
390
-
391
- export default irisDev;
1
+ import chalk from 'chalk';
2
+ import { select, input } from '@inquirer/prompts';
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { resolve, basename, join } from 'node:path';
6
+ import {
7
+ readVueConfig,
8
+ formatMissingConfigError,
9
+ formatConfigErrors,
10
+ injectAssets,
11
+ getInjectionTarget
12
+ } from '../../utils/iris/config-reader.js';
13
+ import { getIrisAssets } from '../../utils/magentrix/api/iris.js';
14
+ import { getAccessToken } from '../../utils/magentrix/api/auth.js';
15
+ import {
16
+ getLinkedProjectsWithStatus,
17
+ buildProjectChoices
18
+ } from '../../utils/iris/linker.js';
19
+ import {
20
+ acquireLock,
21
+ releaseLock,
22
+ LockTypes,
23
+ createProjectContext
24
+ } from '../../utils/iris/lock.js';
25
+ import {
26
+ formatNetworkError,
27
+ formatError
28
+ } from '../../utils/iris/errors.js';
29
+
30
+ /**
31
+ * Timeout for fetching assets (30 seconds).
32
+ */
33
+ const ASSET_FETCH_TIMEOUT = 30000;
34
+
35
+ /**
36
+ * vue-run-dev command - Start Vue dev server with platform assets injected.
37
+ *
38
+ * Uses credentials from .env.development (VITE_REFRESH_TOKEN, VITE_SITE_URL).
39
+ * Assets are updated in .env.development (VITE_ASSETS) and kept between runs.
40
+ *
41
+ * Options:
42
+ * --path <dir> Specify Vue project path
43
+ * --no-inject Skip asset injection, just run dev server
44
+ */
45
+ export const irisDev = async (options = {}) => {
46
+ process.stdout.write('\x1Bc'); // Clear console
47
+
48
+ const { path: pathOption, inject = true } = options;
49
+
50
+ // Determine which project to use
51
+ let projectPath = pathOption;
52
+ let vueConfig = null;
53
+
54
+ if (projectPath) {
55
+ projectPath = resolve(projectPath);
56
+
57
+ if (!existsSync(projectPath)) {
58
+ console.log(chalk.red(`Error: Path does not exist: ${projectPath}`));
59
+ return;
60
+ }
61
+
62
+ vueConfig = readVueConfig(projectPath);
63
+ } else {
64
+ // Try current directory first
65
+ const cwdConfig = readVueConfig(process.cwd());
66
+ if (cwdConfig.found && cwdConfig.errors.length === 0) {
67
+ projectPath = process.cwd();
68
+ vueConfig = cwdConfig;
69
+ console.log(chalk.gray(`Using current directory: ${projectPath}`));
70
+ } else {
71
+ // Prompt user to select a project
72
+ const result = await selectProject();
73
+ if (!result) return;
74
+
75
+ projectPath = result.path;
76
+ vueConfig = result.config;
77
+ }
78
+ }
79
+
80
+ // Validate Vue config
81
+ if (!vueConfig.found) {
82
+ console.log(chalk.red(formatMissingConfigError(projectPath)));
83
+ return;
84
+ }
85
+
86
+ if (vueConfig.errors.length > 0) {
87
+ console.log(chalk.red(formatConfigErrors(vueConfig)));
88
+ return;
89
+ }
90
+
91
+ const { slug, appName, siteUrl } = vueConfig;
92
+
93
+ console.log(chalk.blue('\nIris Development Server'));
94
+ console.log(chalk.gray('─'.repeat(48)));
95
+ console.log(chalk.white(` Project: ${chalk.cyan(appName)} (${slug})`));
96
+ console.log(chalk.white(` Path: ${chalk.gray(projectPath)}`));
97
+ if (siteUrl) {
98
+ console.log(chalk.white(` Site: ${chalk.gray(siteUrl)}`));
99
+ }
100
+ console.log();
101
+
102
+ // Inject assets if enabled
103
+ if (inject && siteUrl) {
104
+ // Check if we have the refresh token for authentication
105
+ if (!vueConfig.refreshToken) {
106
+ console.log(chalk.yellow('Warning: VITE_REFRESH_TOKEN not set in .env.development'));
107
+ console.log(chalk.gray('Asset injection requires authentication. Continuing without assets.'));
108
+ } else {
109
+ console.log(chalk.blue('Fetching platform assets...'));
110
+
111
+ try {
112
+ // Get access token using the refresh token from .env.development
113
+ // Use Promise.race to implement timeout
114
+ const tokenPromise = getAccessToken(vueConfig.refreshToken, siteUrl);
115
+ const timeoutPromise = new Promise((_, reject) =>
116
+ setTimeout(() => reject(new Error('Request timed out')), ASSET_FETCH_TIMEOUT)
117
+ );
118
+
119
+ const tokenData = await Promise.race([tokenPromise, timeoutPromise]);
120
+
121
+ // Fetch assets with timeout
122
+ const assetsPromise = getIrisAssets(siteUrl, tokenData.token);
123
+ const assetsResult = await Promise.race([assetsPromise, timeoutPromise]);
124
+
125
+ if (assetsResult.success && assetsResult.assets?.length > 0) {
126
+ console.log(chalk.green(`\u2713 Found ${assetsResult.assets.length} platform assets`));
127
+
128
+ // Determine which file will be modified
129
+ const { targetFile } = getInjectionTarget(projectPath);
130
+
131
+ if (!targetFile) {
132
+ console.log(chalk.yellow('Warning: No .env.development file found. Cannot inject assets.'));
133
+ console.log(chalk.gray('Create a .env.development file to enable asset injection.'));
134
+ } else {
135
+ // Inject assets (no backup needed - we keep the changes)
136
+ console.log(chalk.blue('Updating assets in .env.development...'));
137
+ const injectResult = injectAssets(projectPath, assetsResult.assets);
138
+
139
+ if (injectResult.success) {
140
+ console.log(chalk.green(`\u2713 Assets updated in ${injectResult.targetName}`));
141
+ } else {
142
+ // Show specific error if available
143
+ if (injectResult.error) {
144
+ console.log(chalk.yellow(`Warning: Could not inject assets: ${injectResult.error}`));
145
+ } else {
146
+ console.log(chalk.yellow('Warning: Could not inject assets. Continuing without injection.'));
147
+ }
148
+ }
149
+ }
150
+ } else if (assetsResult.error) {
151
+ console.log(chalk.yellow(`Warning: Could not fetch assets: ${assetsResult.error}`));
152
+ console.log(chalk.gray('Continuing without asset injection.'));
153
+ } else {
154
+ console.log(chalk.yellow('No platform assets found.'));
155
+ }
156
+ } catch (err) {
157
+ // Provide more helpful error messages based on error type
158
+ if (err.message === 'Request timed out') {
159
+ console.log(chalk.yellow('Warning: Asset fetch timed out.'));
160
+ console.log(chalk.gray('Check your network connection or try again later.'));
161
+ } else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
162
+ console.log(chalk.yellow(formatNetworkError({
163
+ operation: 'fetch assets',
164
+ url: siteUrl,
165
+ error: err
166
+ })));
167
+ } else if (err.message?.includes('401') || err.message?.includes('Unauthorized')) {
168
+ console.log(chalk.yellow('Warning: Authentication failed.'));
169
+ console.log(chalk.gray('Your VITE_REFRESH_TOKEN may be invalid or expired.'));
170
+ console.log(chalk.gray('Get a new API key from your Magentrix platform.'));
171
+ } else {
172
+ console.log(chalk.yellow(`Warning: Error fetching assets: ${err.message}`));
173
+ }
174
+ console.log(chalk.gray('Continuing without asset injection.'));
175
+ }
176
+ }
177
+ } else if (!inject) {
178
+ console.log(chalk.gray('Skipping asset injection (--no-inject)'));
179
+ } else if (!siteUrl) {
180
+ console.log(chalk.yellow('Warning: No siteUrl found. Cannot fetch platform assets.'));
181
+ console.log(chalk.gray('Set VITE_SITE_URL in .env.development'));
182
+ }
183
+
184
+ // Start dev server
185
+ console.log();
186
+ console.log(chalk.blue('Starting development server...'));
187
+ console.log(chalk.gray('Press Ctrl+C to stop'));
188
+ console.log();
189
+
190
+ await runDevServer(projectPath);
191
+ };
192
+
193
+ /**
194
+ * Run the Vue development server.
195
+ *
196
+ * @param {string} projectPath - Path to the Vue project
197
+ * @returns {Promise<number>} - Exit code
198
+ */
199
+ async function runDevServer(projectPath) {
200
+ // Acquire dev server lock to prevent multiple instances
201
+ // Use user home for consistent lock location across projects
202
+ const lockContext = createProjectContext(projectPath);
203
+ const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
204
+ const lockResult = acquireLock(LockTypes.DEV_SERVER, {
205
+ context: lockContext,
206
+ operation: `dev server for ${basename(projectPath)}`,
207
+ basePath: lockBasePath
208
+ });
209
+
210
+ // Track if lock was acquired (permission errors are non-fatal)
211
+ let lockAcquired = lockResult.acquired;
212
+ if (!lockResult.acquired) {
213
+ if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
214
+ console.log(chalk.yellow('Warning: Could not create dev server lock (permission issue). Proceeding without lock.'));
215
+ lockAcquired = false;
216
+ } else {
217
+ console.log(chalk.red('Cannot start dev server:'));
218
+ console.log(chalk.yellow(lockResult.error));
219
+ return 1;
220
+ }
221
+ }
222
+
223
+ return new Promise((resolvePromise) => {
224
+ const isWindows = process.platform === 'win32';
225
+ const npmCmd = isWindows ? 'npm.cmd' : 'npm';
226
+
227
+ const child = spawn(npmCmd, ['run', 'dev'], {
228
+ cwd: projectPath,
229
+ stdio: 'inherit',
230
+ shell: isWindows // Windows requires shell: true for .cmd files
231
+ });
232
+
233
+ // Cleanup function to release lock
234
+ const cleanup = () => {
235
+ if (lockAcquired) {
236
+ releaseLock(LockTypes.DEV_SERVER, { context: lockContext, basePath: lockBasePath });
237
+ }
238
+ };
239
+
240
+ // Handle process signals - use once() to avoid memory leaks
241
+ const handleSignal = (signal) => {
242
+ cleanup();
243
+ child.kill(signal);
244
+ // Give child process a moment to exit, then force exit
245
+ setTimeout(() => {
246
+ process.exit(0);
247
+ }, 500);
248
+ };
249
+
250
+ const sigintHandler = () => handleSignal('SIGINT');
251
+ const sigtermHandler = () => handleSignal('SIGTERM');
252
+
253
+ process.once('SIGINT', sigintHandler);
254
+ process.once('SIGTERM', sigtermHandler);
255
+
256
+ // Remove handlers when child exits to avoid memory leaks
257
+ child.on('exit', () => {
258
+ process.removeListener('SIGINT', sigintHandler);
259
+ process.removeListener('SIGTERM', sigtermHandler);
260
+ });
261
+
262
+ child.on('close', (code) => {
263
+ cleanup();
264
+
265
+ // If dev server exited with error, show helpful context
266
+ if (code !== 0 && code !== null) {
267
+ console.log();
268
+ console.log(chalk.bgYellow.black(' External Process Error '));
269
+ console.log(chalk.yellow('─'.repeat(48)));
270
+ console.log(chalk.white('The Vue dev server exited with an error.'));
271
+ console.log(chalk.gray('This is NOT a MagentrixCLI issue.'));
272
+ console.log();
273
+ console.log(chalk.white('Common causes:'));
274
+ console.log(chalk.gray(' • Permission issues in node_modules/'));
275
+ console.log(chalk.gray(' • Missing dependencies (run npm install)'));
276
+ console.log(chalk.gray(' • Port already in use'));
277
+ console.log(chalk.gray(' • Syntax errors in project files'));
278
+ console.log();
279
+ console.log(chalk.white('If you see EACCES/permission errors, try:'));
280
+ if (isWindows) {
281
+ console.log(chalk.cyan(' Run terminal as Administrator'));
282
+ console.log(chalk.cyan(` rd /s /q "${projectPath}\\node_modules"`));
283
+ console.log(chalk.cyan(' npm install'));
284
+ } else {
285
+ console.log(chalk.cyan(` sudo chown -R $(whoami) "${projectPath}/node_modules"`));
286
+ }
287
+ console.log(chalk.yellow('─'.repeat(48)));
288
+ }
289
+
290
+ resolvePromise(code);
291
+ });
292
+
293
+ child.on('error', (err) => {
294
+ cleanup();
295
+ console.log(chalk.red(`Failed to start dev server: ${err.message}`));
296
+
297
+ // Provide more specific error messages
298
+ if (err.code === 'ENOENT') {
299
+ console.log();
300
+ console.log(chalk.yellow('npm was not found on your system.'));
301
+ console.log(chalk.gray('Make sure Node.js is installed and npm is in your PATH.'));
302
+ } else if (err.code === 'EACCES' || err.code === 'EPERM') {
303
+ console.log();
304
+ console.log(chalk.yellow('Permission denied.'));
305
+ if (isWindows) {
306
+ console.log(chalk.gray('Try running the terminal as Administrator.'));
307
+ } else {
308
+ console.log(chalk.gray('Check file permissions for the project directory.'));
309
+ }
310
+ }
311
+
312
+ resolvePromise(1);
313
+ });
314
+ });
315
+ }
316
+
317
+ /**
318
+ * Prompt user to select a project.
319
+ */
320
+ async function selectProject() {
321
+ const projectsWithStatus = getLinkedProjectsWithStatus();
322
+
323
+ // Check if CWD is a Vue project (and not already linked)
324
+ const cwdConfig = readVueConfig(process.cwd());
325
+ const inVueProject = cwdConfig.found && cwdConfig.errors.length === 0;
326
+ const cwdAlreadyLinked = projectsWithStatus.some(p =>
327
+ resolve(p.path) === resolve(process.cwd())
328
+ );
329
+
330
+ // Build choices using the helper
331
+ const choices = buildProjectChoices({
332
+ includeManual: true,
333
+ includeCancel: true,
334
+ showInvalid: true
335
+ });
336
+
337
+ // If CWD is a valid Vue project and not linked, add it at the top
338
+ if (inVueProject && !cwdAlreadyLinked) {
339
+ choices.unshift({
340
+ name: `${cwdConfig.appName} (${cwdConfig.slug}) - Current directory (not linked)`,
341
+ value: { type: 'cwd', path: process.cwd() }
342
+ });
343
+ }
344
+
345
+ // Check if we have any projects to show
346
+ const hasProjects = choices.some(c => c.value?.type === 'linked' || c.value?.type === 'cwd');
347
+
348
+ if (!hasProjects) {
349
+ console.log(chalk.yellow('No linked Vue projects found.'));
350
+ console.log();
351
+ console.log(chalk.gray('To get started:'));
352
+ console.log(chalk.white(` 1. Link a Vue project: ${chalk.cyan('magentrix iris-app-link')}`));
353
+ console.log(chalk.white(` 2. Or specify path: ${chalk.cyan('magentrix vue-run-dev --path /path/to/vue-project')}`));
354
+ console.log();
355
+ }
356
+
357
+ const choice = await select({
358
+ message: 'Which project do you want to run?',
359
+ choices
360
+ });
361
+
362
+ if (choice.type === 'cancel') {
363
+ console.log(chalk.gray('Cancelled.'));
364
+ return null;
365
+ }
366
+
367
+ if (choice.type === 'manual') {
368
+ const manualPath = await input({
369
+ message: 'Enter the path to your Vue project:',
370
+ validate: (value) => {
371
+ if (!value.trim()) {
372
+ return 'Path is required';
373
+ }
374
+ const resolved = resolve(value);
375
+ if (!existsSync(resolved)) {
376
+ return `Path does not exist: ${resolved}`;
377
+ }
378
+ return true;
379
+ }
380
+ });
381
+
382
+ const config = readVueConfig(resolve(manualPath));
383
+ return { path: resolve(manualPath), config };
384
+ }
385
+
386
+ // Linked project or CWD
387
+ const config = readVueConfig(choice.path);
388
+ return { path: choice.path, config };
389
+ }
390
+
391
+ export default irisDev;