@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
|
@@ -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
|
+
}
|