@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.
- package/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
package/actions/iris/dev.js
CHANGED
|
@@ -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;
|