@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,664 +1,664 @@
1
- import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import { validateSlug, validateUrl, validateAppName, detectExpression, MAX_LENGTHS } from './validation.js';
4
- import { formatError, detectErrorType, ErrorTypes } from './errors.js';
5
-
6
- /**
7
- * Common locations where config.ts might be found in a Vue project.
8
- */
9
- const CONFIG_LOCATIONS = [
10
- 'src/config.ts',
11
- 'config.ts',
12
- 'src/iris-config.ts',
13
- 'iris-config.ts'
14
- ];
15
-
16
- /**
17
- * The .env file to read configuration from.
18
- * Currently only .env.development is supported.
19
- */
20
- const ENV_FILE = '.env.development';
21
-
22
- /**
23
- * Maximum size of assets array before warning.
24
- */
25
- const MAX_ASSETS_WARNING_THRESHOLD = 500;
26
-
27
- /**
28
- * Parse a .env file and return key-value pairs.
29
- *
30
- * @param {string} envPath - Path to the .env file
31
- * @returns {Record<string, string>} - Parsed environment variables
32
- */
33
- function parseEnvFile(envPath) {
34
- const result = {};
35
-
36
- if (!existsSync(envPath)) {
37
- return result;
38
- }
39
-
40
- const content = readFileSync(envPath, 'utf-8');
41
- const lines = content.split('\n');
42
-
43
- for (const line of lines) {
44
- // Skip comments and empty lines
45
- const trimmed = line.trim();
46
- if (!trimmed || trimmed.startsWith('#')) {
47
- continue;
48
- }
49
-
50
- // Parse KEY=VALUE or KEY = VALUE
51
- const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
52
- if (match) {
53
- const key = match[1];
54
- let value = match[2].trim();
55
-
56
- // Remove surrounding quotes if present
57
- if ((value.startsWith('"') && value.endsWith('"')) ||
58
- (value.startsWith("'") && value.endsWith("'"))) {
59
- value = value.slice(1, -1);
60
- }
61
-
62
- result[key] = value;
63
- }
64
- }
65
-
66
- return result;
67
- }
68
-
69
- /**
70
- * Read configuration from .env.development file.
71
- * Reads siteUrl, assets, and refreshToken.
72
- *
73
- * @param {string} projectPath - Path to the Vue project
74
- * @returns {{
75
- * siteUrl: string | null,
76
- * assets: string[],
77
- * refreshToken: string | null,
78
- * envFileUsed: string | null,
79
- * warnings: string[],
80
- * errors: string[]
81
- * }}
82
- */
83
- export function readEnvConfig(projectPath) {
84
- const result = {
85
- siteUrl: null,
86
- assets: [],
87
- refreshToken: null,
88
- envFileUsed: null,
89
- warnings: [],
90
- errors: []
91
- };
92
-
93
- const envPath = join(projectPath, ENV_FILE);
94
-
95
- if (!existsSync(envPath)) {
96
- return result;
97
- }
98
-
99
- let envVars;
100
- try {
101
- envVars = parseEnvFile(envPath);
102
- } catch (err) {
103
- result.errors.push(`Failed to read ${ENV_FILE}: ${err.message}`);
104
- return result;
105
- }
106
-
107
- result.envFileUsed = ENV_FILE;
108
-
109
- // Read and validate VITE_SITE_URL
110
- if (envVars.VITE_SITE_URL) {
111
- const urlValue = envVars.VITE_SITE_URL;
112
- const urlValidation = validateUrl(urlValue);
113
-
114
- if (!urlValidation.valid) {
115
- result.errors.push(`VITE_SITE_URL is invalid: ${urlValidation.error}`);
116
- if (urlValidation.suggestion) {
117
- result.warnings.push(` Suggestion: ${urlValidation.suggestion}`);
118
- }
119
- } else {
120
- result.siteUrl = urlValidation.normalized || urlValue;
121
- }
122
- }
123
-
124
- // Read VITE_ASSETS with better error handling
125
- if (envVars.VITE_ASSETS) {
126
- try {
127
- const parsed = JSON.parse(envVars.VITE_ASSETS);
128
- if (Array.isArray(parsed)) {
129
- result.assets = parsed;
130
-
131
- // Warn if assets array is very large
132
- if (parsed.length > MAX_ASSETS_WARNING_THRESHOLD) {
133
- result.warnings.push(
134
- `VITE_ASSETS contains ${parsed.length} items. ` +
135
- `Large arrays may cause performance issues.`
136
- );
137
- }
138
- } else {
139
- result.warnings.push('VITE_ASSETS is not an array, ignoring');
140
- }
141
- } catch (err) {
142
- // Provide helpful error for JSON parsing issues
143
- if (envVars.VITE_ASSETS.includes("'") && !envVars.VITE_ASSETS.includes('"')) {
144
- result.errors.push(
145
- 'VITE_ASSETS contains invalid JSON. ' +
146
- 'JSON requires double quotes for strings, not single quotes. ' +
147
- 'Example: VITE_ASSETS = \'["url1", "url2"]\''
148
- );
149
- } else {
150
- result.errors.push(`VITE_ASSETS contains invalid JSON: ${err.message}`);
151
- }
152
- }
153
- }
154
-
155
- // Read VITE_REFRESH_TOKEN
156
- if (envVars.VITE_REFRESH_TOKEN) {
157
- result.refreshToken = envVars.VITE_REFRESH_TOKEN;
158
- }
159
-
160
- return result;
161
- }
162
-
163
- /**
164
- * Find the config.ts file in a Vue project.
165
- *
166
- * @param {string} projectPath - Path to the Vue project
167
- * @returns {string | null} - Path to config.ts or null if not found
168
- */
169
- export function findConfigFile(projectPath) {
170
- for (const location of CONFIG_LOCATIONS) {
171
- const fullPath = join(projectPath, location);
172
- if (existsSync(fullPath)) {
173
- return fullPath;
174
- }
175
- }
176
- return null;
177
- }
178
-
179
- /**
180
- * Strip comments from config file content to prevent parsing issues.
181
- * Removes both single-line (//) and multi-line comments.
182
- *
183
- * @param {string} content - Raw config file content
184
- * @returns {string} - Content with comments removed
185
- */
186
- function stripComments(content) {
187
- // Remove single-line comments (but not URLs with //)
188
- // Match // that's not preceded by : (to avoid breaking URLs)
189
- let result = content.replace(/(?<!:)\/\/.*$/gm, '');
190
-
191
- // Remove multi-line comments
192
- result = result.replace(/\/\*[\s\S]*?\*\//g, '');
193
-
194
- return result;
195
- }
196
-
197
- /**
198
- * Parse a config.ts file and extract Iris configuration.
199
- * Uses regex parsing to avoid TypeScript compilation.
200
- *
201
- * @param {string} configPath - Path to the config.ts file
202
- * @returns {{
203
- * slug: string | null,
204
- * appName: string | null,
205
- * appDescription: string | null,
206
- * appIconId: string | null,
207
- * siteUrl: string | null,
208
- * assets: string[],
209
- * raw: string,
210
- * warnings: string[]
211
- * }}
212
- */
213
- export function parseConfigFile(configPath) {
214
- const result = {
215
- slug: null,
216
- appName: null,
217
- appDescription: null,
218
- appIconId: null,
219
- siteUrl: null,
220
- assets: [],
221
- raw: '',
222
- warnings: []
223
- };
224
-
225
- if (!existsSync(configPath)) {
226
- return result;
227
- }
228
-
229
- let content;
230
- try {
231
- content = readFileSync(configPath, 'utf-8');
232
- } catch (err) {
233
- result.warnings.push(`Failed to read config file: ${err.message}`);
234
- return result;
235
- }
236
-
237
- result.raw = content;
238
-
239
- // Strip comments before parsing to avoid regex issues
240
- const cleanContent = stripComments(content);
241
-
242
- // Extract slug (various patterns)
243
- // slug: "dashboard", appSlug: "dashboard", app_slug: "dashboard"
244
- // Also handles: slug: env.slug || "fallback"
245
- const slugMatch = cleanContent.match(/(?:slug|appSlug|app_slug)\s*:\s*["'`]([^"'`]+)["'`]/);
246
- if (slugMatch) {
247
- result.slug = slugMatch[1];
248
-
249
- // Check for template literal expressions
250
- const exprCheck = detectExpression(result.slug);
251
- if (exprCheck.isExpression) {
252
- result.warnings.push(
253
- `slug appears to contain a ${exprCheck.type} (${result.slug}). ` +
254
- `The CLI cannot evaluate dynamic values. ` +
255
- `Use a static string value instead.`
256
- );
257
- result.slug = null;
258
- }
259
- } else {
260
- const slugFallbackMatch = cleanContent.match(/(?:slug|appSlug|app_slug)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
261
- if (slugFallbackMatch) {
262
- result.slug = slugFallbackMatch[1];
263
- }
264
- }
265
-
266
- // Extract appName (various patterns)
267
- // appName: "Dashboard App", app_name: "Dashboard", name: "Dashboard"
268
- // Also handles: appName: env.appName || "fallback"
269
- const appNameMatch = cleanContent.match(/(?:appName|app_name)\s*:\s*["'`]([^"'`]+)["'`]/);
270
- if (appNameMatch) {
271
- result.appName = appNameMatch[1];
272
-
273
- // Check for template literal expressions
274
- const exprCheck = detectExpression(result.appName);
275
- if (exprCheck.isExpression) {
276
- result.warnings.push(
277
- `appName appears to contain a ${exprCheck.type} (${result.appName}). ` +
278
- `The CLI cannot evaluate dynamic values. ` +
279
- `Use a static string value instead.`
280
- );
281
- result.appName = null;
282
- }
283
- } else {
284
- const appNameFallbackMatch = cleanContent.match(/(?:appName|app_name)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
285
- if (appNameFallbackMatch) {
286
- result.appName = appNameFallbackMatch[1];
287
- }
288
- }
289
-
290
- // Extract appDescription (optional)
291
- // appDescription: "Description text"
292
- const appDescriptionMatch = cleanContent.match(/(?:appDescription|app_description)\s*:\s*["'`]([^"'`]*)["'`]/);
293
- if (appDescriptionMatch) {
294
- result.appDescription = appDescriptionMatch[1];
295
- }
296
-
297
- // Extract appIconId (optional)
298
- // appIconId: "icon-id-here"
299
- const appIconIdMatch = cleanContent.match(/(?:appIconId|app_icon_id)\s*:\s*["'`]([^"'`]*)["'`]/);
300
- if (appIconIdMatch) {
301
- result.appIconId = appIconIdMatch[1];
302
- }
303
-
304
- // Extract siteUrl (various patterns)
305
- // siteUrl: "https://...", site_url: "https://...", baseUrl: "https://..."
306
- // Also handles: siteUrl: env.siteUrl || "https://..."
307
- const siteUrlMatch = cleanContent.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*["'`]([^"'`]+)["'`]/);
308
- if (siteUrlMatch) {
309
- result.siteUrl = siteUrlMatch[1];
310
- } else {
311
- // Try to match fallback pattern: siteUrl: env.x || "fallback"
312
- const siteUrlFallbackMatch = cleanContent.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
313
- if (siteUrlFallbackMatch) {
314
- result.siteUrl = siteUrlFallbackMatch[1];
315
- }
316
- }
317
-
318
- // Extract assets array
319
- // assets: ["url1", "url2"]
320
- const assetsMatch = cleanContent.match(/assets\s*:\s*\[([\s\S]*?)\]/);
321
- if (assetsMatch) {
322
- const assetsContent = assetsMatch[1];
323
- // Extract all quoted strings from the array
324
- const urlMatches = assetsContent.matchAll(/["'`]([^"'`]+)["'`]/g);
325
- for (const match of urlMatches) {
326
- result.assets.push(match[1]);
327
- }
328
- }
329
-
330
- return result;
331
- }
332
-
333
- /**
334
- * Read Vue project configuration.
335
- *
336
- * Reads from multiple sources:
337
- * - slug, appName, appDescription, appIconId: from config.ts only
338
- * - siteUrl, assets, refreshToken: from .env.development only (no fallback)
339
- *
340
- * @param {string} projectPath - Path to the Vue project
341
- * @returns {{
342
- * found: boolean,
343
- * configPath: string | null,
344
- * slug: string | null,
345
- * appName: string | null,
346
- * appDescription: string | null,
347
- * appIconId: string | null,
348
- * siteUrl: string | null,
349
- * assets: string[],
350
- * refreshToken: string | null,
351
- * envFileUsed: string | null,
352
- * errors: string[],
353
- * warnings: string[]
354
- * }}
355
- */
356
- export function readVueConfig(projectPath) {
357
- const result = {
358
- found: false,
359
- configPath: null,
360
- slug: null,
361
- appName: null,
362
- appDescription: null,
363
- appIconId: null,
364
- siteUrl: null,
365
- assets: [],
366
- refreshToken: null,
367
- envFileUsed: null,
368
- errors: [],
369
- warnings: []
370
- };
371
-
372
- const configPath = findConfigFile(projectPath);
373
- if (!configPath) {
374
- result.errors.push(`No config.ts found in ${projectPath}`);
375
- result.errors.push(`Searched locations: ${CONFIG_LOCATIONS.join(', ')}`);
376
- return result;
377
- }
378
-
379
- result.found = true;
380
- result.configPath = configPath;
381
-
382
- // Parse config.ts for slug, appName, appDescription, appIconId (always from config.ts)
383
- const parsed = parseConfigFile(configPath);
384
-
385
- // Merge warnings from parsing
386
- if (parsed.warnings?.length > 0) {
387
- result.warnings.push(...parsed.warnings);
388
- }
389
-
390
- result.slug = parsed.slug;
391
- result.appName = parsed.appName;
392
- result.appDescription = parsed.appDescription;
393
- result.appIconId = parsed.appIconId;
394
-
395
- // Validate slug format if present
396
- if (result.slug) {
397
- const slugValidation = validateSlug(result.slug);
398
- if (!slugValidation.valid) {
399
- result.errors.push(`Invalid slug format: ${slugValidation.error}`);
400
- if (slugValidation.suggestion) {
401
- result.warnings.push(` Suggestion: Use "${slugValidation.suggestion}" instead`);
402
- }
403
- // Keep the slug for reference but mark as invalid
404
- }
405
- }
406
-
407
- // Validate appName if present
408
- if (result.appName) {
409
- const appNameValidation = validateAppName(result.appName);
410
- if (!appNameValidation.valid) {
411
- result.errors.push(`Invalid appName: ${appNameValidation.error}`);
412
- }
413
- }
414
-
415
- // Read .env.development for siteUrl, assets, and refreshToken (no fallback to config.ts)
416
- const envConfig = readEnvConfig(projectPath);
417
- result.envFileUsed = envConfig.envFileUsed;
418
- result.siteUrl = envConfig.siteUrl;
419
- result.assets = envConfig.assets;
420
- result.refreshToken = envConfig.refreshToken;
421
-
422
- // Merge env config errors and warnings
423
- if (envConfig.errors?.length > 0) {
424
- result.errors.push(...envConfig.errors);
425
- }
426
- if (envConfig.warnings?.length > 0) {
427
- result.warnings.push(...envConfig.warnings);
428
- }
429
-
430
- // Validate required fields in config.ts
431
- if (!result.slug) {
432
- result.errors.push('Missing required field in config.ts: slug (appSlug)');
433
- }
434
- if (!result.appName) {
435
- result.errors.push('Missing required field in config.ts: appName');
436
- }
437
-
438
- // Warnings for missing .env.development values
439
- if (!envConfig.envFileUsed) {
440
- result.warnings.push('No .env.development file found');
441
- } else {
442
- if (!result.siteUrl && !result.errors.some(e => e.includes('VITE_SITE_URL'))) {
443
- result.warnings.push('VITE_SITE_URL not set in .env.development');
444
- }
445
- }
446
-
447
- return result;
448
- }
449
-
450
- /**
451
- * Create a backup of a file.
452
- *
453
- * @param {string} filePath - Path to the file to backup
454
- * @returns {string} - Path to the backup file
455
- */
456
- export function backupFile(filePath) {
457
- const backupPath = `${filePath}.bak`;
458
- copyFileSync(filePath, backupPath);
459
- return backupPath;
460
- }
461
-
462
- /**
463
- * Restore a file from backup.
464
- *
465
- * @param {string} filePath - Path to the file to restore
466
- * @returns {boolean} - True if restored, false if backup not found
467
- */
468
- export function restoreFile(filePath) {
469
- const backupPath = `${filePath}.bak`;
470
- if (!existsSync(backupPath)) {
471
- return false;
472
- }
473
- copyFileSync(backupPath, filePath);
474
- return true;
475
- }
476
-
477
- /**
478
- * Determine which file will be used for asset injection.
479
- * Only .env.development is supported for asset injection.
480
- *
481
- * @param {string} projectPath - Path to the Vue project
482
- * @returns {{targetFile: string | null, targetName: string | null}}
483
- */
484
- export function getInjectionTarget(projectPath) {
485
- const envPath = join(projectPath, ENV_FILE);
486
-
487
- if (existsSync(envPath)) {
488
- return { targetFile: envPath, targetName: '.env.development' };
489
- }
490
-
491
- return { targetFile: null, targetName: null };
492
- }
493
-
494
- /**
495
- * Inject assets into .env.development file.
496
- * Returns failure if .env.development does not exist.
497
- *
498
- * @param {string} projectPath - Path to the Vue project
499
- * @param {string[]} assets - Array of asset URLs to inject
500
- * @returns {{success: boolean, targetFile: string | null, targetName: string | null, error: string | null}}
501
- */
502
- export function injectAssets(projectPath, assets) {
503
- const { targetFile, targetName } = getInjectionTarget(projectPath);
504
-
505
- if (!targetFile) {
506
- return { success: false, targetFile: null, targetName: null, error: 'No .env.development file found' };
507
- }
508
-
509
- const result = injectAssetsIntoEnv(targetFile, assets);
510
- return {
511
- success: result.success,
512
- targetFile,
513
- targetName,
514
- error: result.error
515
- };
516
- }
517
-
518
- /**
519
- * Inject assets into a .env file.
520
- *
521
- * @param {string} envPath - Path to the .env file
522
- * @param {string[]} assets - Array of asset URLs to inject
523
- * @returns {{success: boolean, error: string | null}}
524
- */
525
- export function injectAssetsIntoEnv(envPath, assets) {
526
- if (!existsSync(envPath)) {
527
- return { success: false, error: 'File does not exist' };
528
- }
529
-
530
- let content;
531
- try {
532
- content = readFileSync(envPath, 'utf-8');
533
- } catch (err) {
534
- const errorType = detectErrorType(err);
535
- if (errorType === ErrorTypes.PERMISSION) {
536
- return {
537
- success: false,
538
- error: `Permission denied reading ${envPath}. Check file permissions.`
539
- };
540
- }
541
- return { success: false, error: `Failed to read file: ${err.message}` };
542
- }
543
-
544
- // JSON stringify handles escaping properly
545
- const assetsJson = JSON.stringify(assets);
546
-
547
- // Escape any single quotes in the JSON to prevent .env parsing issues
548
- // This is extra safety - JSON.stringify shouldn't produce single quotes
549
- const safeJson = assetsJson.replace(/'/g, "\\'");
550
- const newLine = `VITE_ASSETS = '${safeJson}'`;
551
-
552
- // Check if VITE_ASSETS already exists
553
- const assetsPattern = /^VITE_ASSETS\s*=.*$/m;
554
- if (assetsPattern.test(content)) {
555
- // Replace existing VITE_ASSETS line
556
- content = content.replace(assetsPattern, newLine);
557
- } else {
558
- // Add new line at the end
559
- content = content.trimEnd() + '\n' + newLine + '\n';
560
- }
561
-
562
- try {
563
- writeFileSync(envPath, content, 'utf-8');
564
- } catch (err) {
565
- const errorType = detectErrorType(err);
566
- if (errorType === ErrorTypes.PERMISSION) {
567
- return {
568
- success: false,
569
- error: `Permission denied writing to ${envPath}. ` +
570
- `Check file permissions or close any programs using this file.`
571
- };
572
- }
573
- if (errorType === ErrorTypes.DISK_FULL) {
574
- return {
575
- success: false,
576
- error: `Disk full - cannot write to ${envPath}. Free up disk space and try again.`
577
- };
578
- }
579
- return { success: false, error: `Failed to write file: ${err.message}` };
580
- }
581
-
582
- return { success: true, error: null };
583
- }
584
-
585
- /**
586
- * Format missing config error message.
587
- *
588
- * @param {string} projectPath - Path to the Vue project
589
- * @returns {string} - Formatted error message
590
- */
591
- export function formatMissingConfigError(projectPath) {
592
- return `Missing Configuration
593
- ────────────────────────────────────────────────────
594
-
595
- Could not find config.ts in the Vue project.
596
-
597
- Expected location: ${join(projectPath, 'src/config.ts')}
598
-
599
- Required fields in config.ts:
600
- - appSlug (slug): App identifier (used as folder name)
601
- - appName: Display name for navigation menu
602
-
603
- Required fields in .env.development:
604
- - VITE_SITE_URL: Magentrix instance URL
605
- - VITE_REFRESH_TOKEN: API refresh token
606
- - VITE_ASSETS: Platform assets (managed by CLI)
607
-
608
- Example config.ts:
609
- export const config = {
610
- appSlug: "my-app",
611
- appName: "My Application"
612
- }
613
-
614
- Example .env.development:
615
- VITE_SITE_URL = https://yourinstance.magentrix.com
616
- VITE_REFRESH_TOKEN = your-api-key
617
- VITE_ASSETS = '[]'`;
618
- }
619
-
620
- /**
621
- * Format config validation errors and warnings.
622
- *
623
- * @param {ReturnType<typeof readVueConfig>} config - Config read result
624
- * @returns {string} - Formatted error message
625
- */
626
- export function formatConfigErrors(config) {
627
- const lines = [
628
- 'Invalid Configuration',
629
- '────────────────────────────────────────────────────',
630
- '',
631
- `Config file: ${config.configPath || 'not found'}`,
632
- ];
633
-
634
- if (config.envFileUsed) {
635
- lines.push(`Env file: ${config.envFileUsed}`);
636
- } else {
637
- lines.push('Env file: .env.development (not found)');
638
- }
639
-
640
- lines.push('');
641
-
642
- if (config.errors.length > 0) {
643
- lines.push('Errors:');
644
- for (const error of config.errors) {
645
- lines.push(` ✗ ${error}`);
646
- }
647
- lines.push('');
648
- }
649
-
650
- if (config.warnings?.length > 0) {
651
- lines.push('Warnings:');
652
- for (const warning of config.warnings) {
653
- lines.push(` ⚠ ${warning}`);
654
- }
655
- lines.push('');
656
- }
657
-
658
- lines.push('Current values:');
659
- lines.push(` slug: ${config.slug || '(missing)'}`);
660
- lines.push(` appName: ${config.appName || '(missing)'}`);
661
- lines.push(` siteUrl: ${config.siteUrl || '(not set)'}`);
662
-
663
- return lines.join('\n');
664
- }
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { validateSlug, validateUrl, validateAppName, detectExpression, MAX_LENGTHS } from './validation.js';
4
+ import { formatError, detectErrorType, ErrorTypes } from './errors.js';
5
+
6
+ /**
7
+ * Common locations where config.ts might be found in a Vue project.
8
+ */
9
+ const CONFIG_LOCATIONS = [
10
+ 'src/config.ts',
11
+ 'config.ts',
12
+ 'src/iris-config.ts',
13
+ 'iris-config.ts'
14
+ ];
15
+
16
+ /**
17
+ * The .env file to read configuration from.
18
+ * Currently only .env.development is supported.
19
+ */
20
+ const ENV_FILE = '.env.development';
21
+
22
+ /**
23
+ * Maximum size of assets array before warning.
24
+ */
25
+ const MAX_ASSETS_WARNING_THRESHOLD = 500;
26
+
27
+ /**
28
+ * Parse a .env file and return key-value pairs.
29
+ *
30
+ * @param {string} envPath - Path to the .env file
31
+ * @returns {Record<string, string>} - Parsed environment variables
32
+ */
33
+ function parseEnvFile(envPath) {
34
+ const result = {};
35
+
36
+ if (!existsSync(envPath)) {
37
+ return result;
38
+ }
39
+
40
+ const content = readFileSync(envPath, 'utf-8');
41
+ const lines = content.split('\n');
42
+
43
+ for (const line of lines) {
44
+ // Skip comments and empty lines
45
+ const trimmed = line.trim();
46
+ if (!trimmed || trimmed.startsWith('#')) {
47
+ continue;
48
+ }
49
+
50
+ // Parse KEY=VALUE or KEY = VALUE
51
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
52
+ if (match) {
53
+ const key = match[1];
54
+ let value = match[2].trim();
55
+
56
+ // Remove surrounding quotes if present
57
+ if ((value.startsWith('"') && value.endsWith('"')) ||
58
+ (value.startsWith("'") && value.endsWith("'"))) {
59
+ value = value.slice(1, -1);
60
+ }
61
+
62
+ result[key] = value;
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Read configuration from .env.development file.
71
+ * Reads siteUrl, assets, and refreshToken.
72
+ *
73
+ * @param {string} projectPath - Path to the Vue project
74
+ * @returns {{
75
+ * siteUrl: string | null,
76
+ * assets: string[],
77
+ * refreshToken: string | null,
78
+ * envFileUsed: string | null,
79
+ * warnings: string[],
80
+ * errors: string[]
81
+ * }}
82
+ */
83
+ export function readEnvConfig(projectPath) {
84
+ const result = {
85
+ siteUrl: null,
86
+ assets: [],
87
+ refreshToken: null,
88
+ envFileUsed: null,
89
+ warnings: [],
90
+ errors: []
91
+ };
92
+
93
+ const envPath = join(projectPath, ENV_FILE);
94
+
95
+ if (!existsSync(envPath)) {
96
+ return result;
97
+ }
98
+
99
+ let envVars;
100
+ try {
101
+ envVars = parseEnvFile(envPath);
102
+ } catch (err) {
103
+ result.errors.push(`Failed to read ${ENV_FILE}: ${err.message}`);
104
+ return result;
105
+ }
106
+
107
+ result.envFileUsed = ENV_FILE;
108
+
109
+ // Read and validate VITE_SITE_URL
110
+ if (envVars.VITE_SITE_URL) {
111
+ const urlValue = envVars.VITE_SITE_URL;
112
+ const urlValidation = validateUrl(urlValue);
113
+
114
+ if (!urlValidation.valid) {
115
+ result.errors.push(`VITE_SITE_URL is invalid: ${urlValidation.error}`);
116
+ if (urlValidation.suggestion) {
117
+ result.warnings.push(` Suggestion: ${urlValidation.suggestion}`);
118
+ }
119
+ } else {
120
+ result.siteUrl = urlValidation.normalized || urlValue;
121
+ }
122
+ }
123
+
124
+ // Read VITE_ASSETS with better error handling
125
+ if (envVars.VITE_ASSETS) {
126
+ try {
127
+ const parsed = JSON.parse(envVars.VITE_ASSETS);
128
+ if (Array.isArray(parsed)) {
129
+ result.assets = parsed;
130
+
131
+ // Warn if assets array is very large
132
+ if (parsed.length > MAX_ASSETS_WARNING_THRESHOLD) {
133
+ result.warnings.push(
134
+ `VITE_ASSETS contains ${parsed.length} items. ` +
135
+ `Large arrays may cause performance issues.`
136
+ );
137
+ }
138
+ } else {
139
+ result.warnings.push('VITE_ASSETS is not an array, ignoring');
140
+ }
141
+ } catch (err) {
142
+ // Provide helpful error for JSON parsing issues
143
+ if (envVars.VITE_ASSETS.includes("'") && !envVars.VITE_ASSETS.includes('"')) {
144
+ result.errors.push(
145
+ 'VITE_ASSETS contains invalid JSON. ' +
146
+ 'JSON requires double quotes for strings, not single quotes. ' +
147
+ 'Example: VITE_ASSETS = \'["url1", "url2"]\''
148
+ );
149
+ } else {
150
+ result.errors.push(`VITE_ASSETS contains invalid JSON: ${err.message}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ // Read VITE_REFRESH_TOKEN
156
+ if (envVars.VITE_REFRESH_TOKEN) {
157
+ result.refreshToken = envVars.VITE_REFRESH_TOKEN;
158
+ }
159
+
160
+ return result;
161
+ }
162
+
163
+ /**
164
+ * Find the config.ts file in a Vue project.
165
+ *
166
+ * @param {string} projectPath - Path to the Vue project
167
+ * @returns {string | null} - Path to config.ts or null if not found
168
+ */
169
+ export function findConfigFile(projectPath) {
170
+ for (const location of CONFIG_LOCATIONS) {
171
+ const fullPath = join(projectPath, location);
172
+ if (existsSync(fullPath)) {
173
+ return fullPath;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * Strip comments from config file content to prevent parsing issues.
181
+ * Removes both single-line (//) and multi-line comments.
182
+ *
183
+ * @param {string} content - Raw config file content
184
+ * @returns {string} - Content with comments removed
185
+ */
186
+ function stripComments(content) {
187
+ // Remove single-line comments (but not URLs with //)
188
+ // Match // that's not preceded by : (to avoid breaking URLs)
189
+ let result = content.replace(/(?<!:)\/\/.*$/gm, '');
190
+
191
+ // Remove multi-line comments
192
+ result = result.replace(/\/\*[\s\S]*?\*\//g, '');
193
+
194
+ return result;
195
+ }
196
+
197
+ /**
198
+ * Parse a config.ts file and extract Iris configuration.
199
+ * Uses regex parsing to avoid TypeScript compilation.
200
+ *
201
+ * @param {string} configPath - Path to the config.ts file
202
+ * @returns {{
203
+ * slug: string | null,
204
+ * appName: string | null,
205
+ * appDescription: string | null,
206
+ * appIconId: string | null,
207
+ * siteUrl: string | null,
208
+ * assets: string[],
209
+ * raw: string,
210
+ * warnings: string[]
211
+ * }}
212
+ */
213
+ export function parseConfigFile(configPath) {
214
+ const result = {
215
+ slug: null,
216
+ appName: null,
217
+ appDescription: null,
218
+ appIconId: null,
219
+ siteUrl: null,
220
+ assets: [],
221
+ raw: '',
222
+ warnings: []
223
+ };
224
+
225
+ if (!existsSync(configPath)) {
226
+ return result;
227
+ }
228
+
229
+ let content;
230
+ try {
231
+ content = readFileSync(configPath, 'utf-8');
232
+ } catch (err) {
233
+ result.warnings.push(`Failed to read config file: ${err.message}`);
234
+ return result;
235
+ }
236
+
237
+ result.raw = content;
238
+
239
+ // Strip comments before parsing to avoid regex issues
240
+ const cleanContent = stripComments(content);
241
+
242
+ // Extract slug (various patterns)
243
+ // slug: "dashboard", appSlug: "dashboard", app_slug: "dashboard"
244
+ // Also handles: slug: env.slug || "fallback"
245
+ const slugMatch = cleanContent.match(/(?:slug|appSlug|app_slug)\s*:\s*["'`]([^"'`]+)["'`]/);
246
+ if (slugMatch) {
247
+ result.slug = slugMatch[1];
248
+
249
+ // Check for template literal expressions
250
+ const exprCheck = detectExpression(result.slug);
251
+ if (exprCheck.isExpression) {
252
+ result.warnings.push(
253
+ `slug appears to contain a ${exprCheck.type} (${result.slug}). ` +
254
+ `The CLI cannot evaluate dynamic values. ` +
255
+ `Use a static string value instead.`
256
+ );
257
+ result.slug = null;
258
+ }
259
+ } else {
260
+ const slugFallbackMatch = cleanContent.match(/(?:slug|appSlug|app_slug)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
261
+ if (slugFallbackMatch) {
262
+ result.slug = slugFallbackMatch[1];
263
+ }
264
+ }
265
+
266
+ // Extract appName (various patterns)
267
+ // appName: "Dashboard App", app_name: "Dashboard", name: "Dashboard"
268
+ // Also handles: appName: env.appName || "fallback"
269
+ const appNameMatch = cleanContent.match(/(?:appName|app_name)\s*:\s*["'`]([^"'`]+)["'`]/);
270
+ if (appNameMatch) {
271
+ result.appName = appNameMatch[1];
272
+
273
+ // Check for template literal expressions
274
+ const exprCheck = detectExpression(result.appName);
275
+ if (exprCheck.isExpression) {
276
+ result.warnings.push(
277
+ `appName appears to contain a ${exprCheck.type} (${result.appName}). ` +
278
+ `The CLI cannot evaluate dynamic values. ` +
279
+ `Use a static string value instead.`
280
+ );
281
+ result.appName = null;
282
+ }
283
+ } else {
284
+ const appNameFallbackMatch = cleanContent.match(/(?:appName|app_name)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
285
+ if (appNameFallbackMatch) {
286
+ result.appName = appNameFallbackMatch[1];
287
+ }
288
+ }
289
+
290
+ // Extract appDescription (optional)
291
+ // appDescription: "Description text"
292
+ const appDescriptionMatch = cleanContent.match(/(?:appDescription|app_description)\s*:\s*["'`]([^"'`]*)["'`]/);
293
+ if (appDescriptionMatch) {
294
+ result.appDescription = appDescriptionMatch[1];
295
+ }
296
+
297
+ // Extract appIconId (optional)
298
+ // appIconId: "icon-id-here"
299
+ const appIconIdMatch = cleanContent.match(/(?:appIconId|app_icon_id)\s*:\s*["'`]([^"'`]*)["'`]/);
300
+ if (appIconIdMatch) {
301
+ result.appIconId = appIconIdMatch[1];
302
+ }
303
+
304
+ // Extract siteUrl (various patterns)
305
+ // siteUrl: "https://...", site_url: "https://...", baseUrl: "https://..."
306
+ // Also handles: siteUrl: env.siteUrl || "https://..."
307
+ const siteUrlMatch = cleanContent.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*["'`]([^"'`]+)["'`]/);
308
+ if (siteUrlMatch) {
309
+ result.siteUrl = siteUrlMatch[1];
310
+ } else {
311
+ // Try to match fallback pattern: siteUrl: env.x || "fallback"
312
+ const siteUrlFallbackMatch = cleanContent.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
313
+ if (siteUrlFallbackMatch) {
314
+ result.siteUrl = siteUrlFallbackMatch[1];
315
+ }
316
+ }
317
+
318
+ // Extract assets array
319
+ // assets: ["url1", "url2"]
320
+ const assetsMatch = cleanContent.match(/assets\s*:\s*\[([\s\S]*?)\]/);
321
+ if (assetsMatch) {
322
+ const assetsContent = assetsMatch[1];
323
+ // Extract all quoted strings from the array
324
+ const urlMatches = assetsContent.matchAll(/["'`]([^"'`]+)["'`]/g);
325
+ for (const match of urlMatches) {
326
+ result.assets.push(match[1]);
327
+ }
328
+ }
329
+
330
+ return result;
331
+ }
332
+
333
+ /**
334
+ * Read Vue project configuration.
335
+ *
336
+ * Reads from multiple sources:
337
+ * - slug, appName, appDescription, appIconId: from config.ts only
338
+ * - siteUrl, assets, refreshToken: from .env.development only (no fallback)
339
+ *
340
+ * @param {string} projectPath - Path to the Vue project
341
+ * @returns {{
342
+ * found: boolean,
343
+ * configPath: string | null,
344
+ * slug: string | null,
345
+ * appName: string | null,
346
+ * appDescription: string | null,
347
+ * appIconId: string | null,
348
+ * siteUrl: string | null,
349
+ * assets: string[],
350
+ * refreshToken: string | null,
351
+ * envFileUsed: string | null,
352
+ * errors: string[],
353
+ * warnings: string[]
354
+ * }}
355
+ */
356
+ export function readVueConfig(projectPath) {
357
+ const result = {
358
+ found: false,
359
+ configPath: null,
360
+ slug: null,
361
+ appName: null,
362
+ appDescription: null,
363
+ appIconId: null,
364
+ siteUrl: null,
365
+ assets: [],
366
+ refreshToken: null,
367
+ envFileUsed: null,
368
+ errors: [],
369
+ warnings: []
370
+ };
371
+
372
+ const configPath = findConfigFile(projectPath);
373
+ if (!configPath) {
374
+ result.errors.push(`No config.ts found in ${projectPath}`);
375
+ result.errors.push(`Searched locations: ${CONFIG_LOCATIONS.join(', ')}`);
376
+ return result;
377
+ }
378
+
379
+ result.found = true;
380
+ result.configPath = configPath;
381
+
382
+ // Parse config.ts for slug, appName, appDescription, appIconId (always from config.ts)
383
+ const parsed = parseConfigFile(configPath);
384
+
385
+ // Merge warnings from parsing
386
+ if (parsed.warnings?.length > 0) {
387
+ result.warnings.push(...parsed.warnings);
388
+ }
389
+
390
+ result.slug = parsed.slug;
391
+ result.appName = parsed.appName;
392
+ result.appDescription = parsed.appDescription;
393
+ result.appIconId = parsed.appIconId;
394
+
395
+ // Validate slug format if present
396
+ if (result.slug) {
397
+ const slugValidation = validateSlug(result.slug);
398
+ if (!slugValidation.valid) {
399
+ result.errors.push(`Invalid slug format: ${slugValidation.error}`);
400
+ if (slugValidation.suggestion) {
401
+ result.warnings.push(` Suggestion: Use "${slugValidation.suggestion}" instead`);
402
+ }
403
+ // Keep the slug for reference but mark as invalid
404
+ }
405
+ }
406
+
407
+ // Validate appName if present
408
+ if (result.appName) {
409
+ const appNameValidation = validateAppName(result.appName);
410
+ if (!appNameValidation.valid) {
411
+ result.errors.push(`Invalid appName: ${appNameValidation.error}`);
412
+ }
413
+ }
414
+
415
+ // Read .env.development for siteUrl, assets, and refreshToken (no fallback to config.ts)
416
+ const envConfig = readEnvConfig(projectPath);
417
+ result.envFileUsed = envConfig.envFileUsed;
418
+ result.siteUrl = envConfig.siteUrl;
419
+ result.assets = envConfig.assets;
420
+ result.refreshToken = envConfig.refreshToken;
421
+
422
+ // Merge env config errors and warnings
423
+ if (envConfig.errors?.length > 0) {
424
+ result.errors.push(...envConfig.errors);
425
+ }
426
+ if (envConfig.warnings?.length > 0) {
427
+ result.warnings.push(...envConfig.warnings);
428
+ }
429
+
430
+ // Validate required fields in config.ts
431
+ if (!result.slug) {
432
+ result.errors.push('Missing required field in config.ts: slug (appSlug)');
433
+ }
434
+ if (!result.appName) {
435
+ result.errors.push('Missing required field in config.ts: appName');
436
+ }
437
+
438
+ // Warnings for missing .env.development values
439
+ if (!envConfig.envFileUsed) {
440
+ result.warnings.push('No .env.development file found');
441
+ } else {
442
+ if (!result.siteUrl && !result.errors.some(e => e.includes('VITE_SITE_URL'))) {
443
+ result.warnings.push('VITE_SITE_URL not set in .env.development');
444
+ }
445
+ }
446
+
447
+ return result;
448
+ }
449
+
450
+ /**
451
+ * Create a backup of a file.
452
+ *
453
+ * @param {string} filePath - Path to the file to backup
454
+ * @returns {string} - Path to the backup file
455
+ */
456
+ export function backupFile(filePath) {
457
+ const backupPath = `${filePath}.bak`;
458
+ copyFileSync(filePath, backupPath);
459
+ return backupPath;
460
+ }
461
+
462
+ /**
463
+ * Restore a file from backup.
464
+ *
465
+ * @param {string} filePath - Path to the file to restore
466
+ * @returns {boolean} - True if restored, false if backup not found
467
+ */
468
+ export function restoreFile(filePath) {
469
+ const backupPath = `${filePath}.bak`;
470
+ if (!existsSync(backupPath)) {
471
+ return false;
472
+ }
473
+ copyFileSync(backupPath, filePath);
474
+ return true;
475
+ }
476
+
477
+ /**
478
+ * Determine which file will be used for asset injection.
479
+ * Only .env.development is supported for asset injection.
480
+ *
481
+ * @param {string} projectPath - Path to the Vue project
482
+ * @returns {{targetFile: string | null, targetName: string | null}}
483
+ */
484
+ export function getInjectionTarget(projectPath) {
485
+ const envPath = join(projectPath, ENV_FILE);
486
+
487
+ if (existsSync(envPath)) {
488
+ return { targetFile: envPath, targetName: '.env.development' };
489
+ }
490
+
491
+ return { targetFile: null, targetName: null };
492
+ }
493
+
494
+ /**
495
+ * Inject assets into .env.development file.
496
+ * Returns failure if .env.development does not exist.
497
+ *
498
+ * @param {string} projectPath - Path to the Vue project
499
+ * @param {string[]} assets - Array of asset URLs to inject
500
+ * @returns {{success: boolean, targetFile: string | null, targetName: string | null, error: string | null}}
501
+ */
502
+ export function injectAssets(projectPath, assets) {
503
+ const { targetFile, targetName } = getInjectionTarget(projectPath);
504
+
505
+ if (!targetFile) {
506
+ return { success: false, targetFile: null, targetName: null, error: 'No .env.development file found' };
507
+ }
508
+
509
+ const result = injectAssetsIntoEnv(targetFile, assets);
510
+ return {
511
+ success: result.success,
512
+ targetFile,
513
+ targetName,
514
+ error: result.error
515
+ };
516
+ }
517
+
518
+ /**
519
+ * Inject assets into a .env file.
520
+ *
521
+ * @param {string} envPath - Path to the .env file
522
+ * @param {string[]} assets - Array of asset URLs to inject
523
+ * @returns {{success: boolean, error: string | null}}
524
+ */
525
+ export function injectAssetsIntoEnv(envPath, assets) {
526
+ if (!existsSync(envPath)) {
527
+ return { success: false, error: 'File does not exist' };
528
+ }
529
+
530
+ let content;
531
+ try {
532
+ content = readFileSync(envPath, 'utf-8');
533
+ } catch (err) {
534
+ const errorType = detectErrorType(err);
535
+ if (errorType === ErrorTypes.PERMISSION) {
536
+ return {
537
+ success: false,
538
+ error: `Permission denied reading ${envPath}. Check file permissions.`
539
+ };
540
+ }
541
+ return { success: false, error: `Failed to read file: ${err.message}` };
542
+ }
543
+
544
+ // JSON stringify handles escaping properly
545
+ const assetsJson = JSON.stringify(assets);
546
+
547
+ // Escape any single quotes in the JSON to prevent .env parsing issues
548
+ // This is extra safety - JSON.stringify shouldn't produce single quotes
549
+ const safeJson = assetsJson.replace(/'/g, "\\'");
550
+ const newLine = `VITE_ASSETS = '${safeJson}'`;
551
+
552
+ // Check if VITE_ASSETS already exists
553
+ const assetsPattern = /^VITE_ASSETS\s*=.*$/m;
554
+ if (assetsPattern.test(content)) {
555
+ // Replace existing VITE_ASSETS line
556
+ content = content.replace(assetsPattern, newLine);
557
+ } else {
558
+ // Add new line at the end
559
+ content = content.trimEnd() + '\n' + newLine + '\n';
560
+ }
561
+
562
+ try {
563
+ writeFileSync(envPath, content, 'utf-8');
564
+ } catch (err) {
565
+ const errorType = detectErrorType(err);
566
+ if (errorType === ErrorTypes.PERMISSION) {
567
+ return {
568
+ success: false,
569
+ error: `Permission denied writing to ${envPath}. ` +
570
+ `Check file permissions or close any programs using this file.`
571
+ };
572
+ }
573
+ if (errorType === ErrorTypes.DISK_FULL) {
574
+ return {
575
+ success: false,
576
+ error: `Disk full - cannot write to ${envPath}. Free up disk space and try again.`
577
+ };
578
+ }
579
+ return { success: false, error: `Failed to write file: ${err.message}` };
580
+ }
581
+
582
+ return { success: true, error: null };
583
+ }
584
+
585
+ /**
586
+ * Format missing config error message.
587
+ *
588
+ * @param {string} projectPath - Path to the Vue project
589
+ * @returns {string} - Formatted error message
590
+ */
591
+ export function formatMissingConfigError(projectPath) {
592
+ return `Missing Configuration
593
+ ────────────────────────────────────────────────────
594
+
595
+ Could not find config.ts in the Vue project.
596
+
597
+ Expected location: ${join(projectPath, 'src/config.ts')}
598
+
599
+ Required fields in config.ts:
600
+ - appSlug (slug): App identifier (used as folder name)
601
+ - appName: Display name for navigation menu
602
+
603
+ Required fields in .env.development:
604
+ - VITE_SITE_URL: Magentrix instance URL
605
+ - VITE_REFRESH_TOKEN: API refresh token
606
+ - VITE_ASSETS: Platform assets (managed by CLI)
607
+
608
+ Example config.ts:
609
+ export const config = {
610
+ appSlug: "my-app",
611
+ appName: "My Application"
612
+ }
613
+
614
+ Example .env.development:
615
+ VITE_SITE_URL = https://yourinstance.magentrix.com
616
+ VITE_REFRESH_TOKEN = your-api-key
617
+ VITE_ASSETS = '[]'`;
618
+ }
619
+
620
+ /**
621
+ * Format config validation errors and warnings.
622
+ *
623
+ * @param {ReturnType<typeof readVueConfig>} config - Config read result
624
+ * @returns {string} - Formatted error message
625
+ */
626
+ export function formatConfigErrors(config) {
627
+ const lines = [
628
+ 'Invalid Configuration',
629
+ '────────────────────────────────────────────────────',
630
+ '',
631
+ `Config file: ${config.configPath || 'not found'}`,
632
+ ];
633
+
634
+ if (config.envFileUsed) {
635
+ lines.push(`Env file: ${config.envFileUsed}`);
636
+ } else {
637
+ lines.push('Env file: .env.development (not found)');
638
+ }
639
+
640
+ lines.push('');
641
+
642
+ if (config.errors.length > 0) {
643
+ lines.push('Errors:');
644
+ for (const error of config.errors) {
645
+ lines.push(` ✗ ${error}`);
646
+ }
647
+ lines.push('');
648
+ }
649
+
650
+ if (config.warnings?.length > 0) {
651
+ lines.push('Warnings:');
652
+ for (const warning of config.warnings) {
653
+ lines.push(` ⚠ ${warning}`);
654
+ }
655
+ lines.push('');
656
+ }
657
+
658
+ lines.push('Current values:');
659
+ lines.push(` slug: ${config.slug || '(missing)'}`);
660
+ lines.push(` appName: ${config.appName || '(missing)'}`);
661
+ lines.push(` siteUrl: ${config.siteUrl || '(not set)'}`);
662
+
663
+ return lines.join('\n');
664
+ }