@magentrix-corp/magentrix-cli 1.2.1 → 1.3.0

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.
@@ -0,0 +1,296 @@
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+
4
+ /**
5
+ * Common locations where config.ts might be found in a Vue project.
6
+ */
7
+ const CONFIG_LOCATIONS = [
8
+ 'src/config.ts',
9
+ 'config.ts',
10
+ 'src/iris-config.ts',
11
+ 'iris-config.ts'
12
+ ];
13
+
14
+ /**
15
+ * Find the config.ts file in a Vue project.
16
+ *
17
+ * @param {string} projectPath - Path to the Vue project
18
+ * @returns {string | null} - Path to config.ts or null if not found
19
+ */
20
+ export function findConfigFile(projectPath) {
21
+ for (const location of CONFIG_LOCATIONS) {
22
+ const fullPath = join(projectPath, location);
23
+ if (existsSync(fullPath)) {
24
+ return fullPath;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Parse a config.ts file and extract Iris configuration.
32
+ * Uses regex parsing to avoid TypeScript compilation.
33
+ *
34
+ * @param {string} configPath - Path to the config.ts file
35
+ * @returns {{
36
+ * slug: string | null,
37
+ * appName: string | null,
38
+ * siteUrl: string | null,
39
+ * assets: string[],
40
+ * raw: string
41
+ * }}
42
+ */
43
+ export function parseConfigFile(configPath) {
44
+ const result = {
45
+ slug: null,
46
+ appName: null,
47
+ siteUrl: null,
48
+ assets: [],
49
+ raw: ''
50
+ };
51
+
52
+ if (!existsSync(configPath)) {
53
+ return result;
54
+ }
55
+
56
+ const content = readFileSync(configPath, 'utf-8');
57
+ result.raw = content;
58
+
59
+ // Extract slug (various patterns)
60
+ // slug: "dashboard", appPath: "dashboard", app_path: "dashboard"
61
+ // Also handles: slug: env.slug || "fallback"
62
+ const slugMatch = content.match(/(?:slug|appPath|app_path)\s*:\s*["'`]([^"'`]+)["'`]/);
63
+ if (slugMatch) {
64
+ result.slug = slugMatch[1];
65
+ } else {
66
+ const slugFallbackMatch = content.match(/(?:slug|appPath|app_path)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
67
+ if (slugFallbackMatch) {
68
+ result.slug = slugFallbackMatch[1];
69
+ }
70
+ }
71
+
72
+ // Extract appName (various patterns)
73
+ // appName: "Dashboard App", app_name: "Dashboard", name: "Dashboard"
74
+ // Also handles: appName: env.appName || "fallback"
75
+ const appNameMatch = content.match(/(?:appName|app_name)\s*:\s*["'`]([^"'`]+)["'`]/);
76
+ if (appNameMatch) {
77
+ result.appName = appNameMatch[1];
78
+ } else {
79
+ const appNameFallbackMatch = content.match(/(?:appName|app_name)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
80
+ if (appNameFallbackMatch) {
81
+ result.appName = appNameFallbackMatch[1];
82
+ }
83
+ }
84
+
85
+ // Extract siteUrl (various patterns)
86
+ // siteUrl: "https://...", site_url: "https://...", baseUrl: "https://..."
87
+ // Also handles: siteUrl: env.siteUrl || "https://..."
88
+ const siteUrlMatch = content.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*["'`]([^"'`]+)["'`]/);
89
+ if (siteUrlMatch) {
90
+ result.siteUrl = siteUrlMatch[1];
91
+ } else {
92
+ // Try to match fallback pattern: siteUrl: env.x || "fallback"
93
+ const siteUrlFallbackMatch = content.match(/(?:siteUrl|site_url|baseUrl|base_url)\s*:\s*[^,\n]+\|\|\s*["'`]([^"'`]+)["'`]/);
94
+ if (siteUrlFallbackMatch) {
95
+ result.siteUrl = siteUrlFallbackMatch[1];
96
+ }
97
+ }
98
+
99
+ // Extract assets array
100
+ // assets: ["url1", "url2"]
101
+ const assetsMatch = content.match(/assets\s*:\s*\[([\s\S]*?)\]/);
102
+ if (assetsMatch) {
103
+ const assetsContent = assetsMatch[1];
104
+ // Extract all quoted strings from the array
105
+ const urlMatches = assetsContent.matchAll(/["'`]([^"'`]+)["'`]/g);
106
+ for (const match of urlMatches) {
107
+ result.assets.push(match[1]);
108
+ }
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Read Vue project configuration.
116
+ *
117
+ * @param {string} projectPath - Path to the Vue project
118
+ * @returns {{
119
+ * found: boolean,
120
+ * configPath: string | null,
121
+ * slug: string | null,
122
+ * appName: string | null,
123
+ * siteUrl: string | null,
124
+ * assets: string[],
125
+ * errors: string[]
126
+ * }}
127
+ */
128
+ export function readVueConfig(projectPath) {
129
+ const result = {
130
+ found: false,
131
+ configPath: null,
132
+ slug: null,
133
+ appName: null,
134
+ siteUrl: null,
135
+ assets: [],
136
+ errors: []
137
+ };
138
+
139
+ const configPath = findConfigFile(projectPath);
140
+ if (!configPath) {
141
+ result.errors.push(`No config.ts found in ${projectPath}`);
142
+ result.errors.push(`Searched locations: ${CONFIG_LOCATIONS.join(', ')}`);
143
+ return result;
144
+ }
145
+
146
+ result.found = true;
147
+ result.configPath = configPath;
148
+
149
+ const parsed = parseConfigFile(configPath);
150
+ result.slug = parsed.slug;
151
+ result.appName = parsed.appName;
152
+ result.siteUrl = parsed.siteUrl;
153
+ result.assets = parsed.assets;
154
+
155
+ // Validate required fields
156
+ if (!result.slug) {
157
+ result.errors.push('Missing required field: slug');
158
+ }
159
+ if (!result.appName) {
160
+ result.errors.push('Missing required field: appName');
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Create a backup of the config file.
168
+ *
169
+ * @param {string} configPath - Path to the config.ts file
170
+ * @returns {string} - Path to the backup file
171
+ */
172
+ export function backupConfigFile(configPath) {
173
+ const backupPath = `${configPath}.bak`;
174
+ copyFileSync(configPath, backupPath);
175
+ return backupPath;
176
+ }
177
+
178
+ /**
179
+ * Restore config file from backup.
180
+ *
181
+ * @param {string} configPath - Path to the config.ts file
182
+ * @returns {boolean} - True if restored, false if backup not found
183
+ */
184
+ export function restoreConfigFile(configPath) {
185
+ const backupPath = `${configPath}.bak`;
186
+ if (!existsSync(backupPath)) {
187
+ return false;
188
+ }
189
+ copyFileSync(backupPath, configPath);
190
+ return true;
191
+ }
192
+
193
+ /**
194
+ * Inject assets into config.ts file.
195
+ *
196
+ * @param {string} configPath - Path to the config.ts file
197
+ * @param {string[]} assets - Array of asset URLs to inject
198
+ * @returns {boolean} - True if successful
199
+ */
200
+ export function injectAssetsIntoConfig(configPath, assets) {
201
+ if (!existsSync(configPath)) {
202
+ return false;
203
+ }
204
+
205
+ let content = readFileSync(configPath, 'utf-8');
206
+
207
+ // Format assets array
208
+ const assetsStr = assets.map(url => ` "${url}"`).join(',\n');
209
+ const newAssetsBlock = `assets: [\n // Injected by magentrix iris-dev\n${assetsStr}\n ]`;
210
+
211
+ // Check if assets array exists
212
+ const assetsPattern = /assets\s*:\s*\[[\s\S]*?\]/;
213
+ if (assetsPattern.test(content)) {
214
+ // Replace existing assets array
215
+ content = content.replace(assetsPattern, newAssetsBlock);
216
+ } else {
217
+ // Need to add assets array - find a good place
218
+ // Look for the end of the config object
219
+ const configObjectPattern = /(export\s+(?:const|let|var)\s+\w+\s*=\s*\{[\s\S]*?)(}\s*;?\s*$)/;
220
+ const match = content.match(configObjectPattern);
221
+ if (match) {
222
+ // Insert before closing brace
223
+ const beforeClose = match[1].trimEnd();
224
+ const needsComma = !beforeClose.endsWith(',') && !beforeClose.endsWith('{');
225
+ content = content.replace(
226
+ configObjectPattern,
227
+ `${match[1]}${needsComma ? ',' : ''}\n ${newAssetsBlock}\n${match[2]}`
228
+ );
229
+ } else {
230
+ // Can't find a good place to inject
231
+ return false;
232
+ }
233
+ }
234
+
235
+ writeFileSync(configPath, content, 'utf-8');
236
+ return true;
237
+ }
238
+
239
+ /**
240
+ * Format missing config error message.
241
+ *
242
+ * @param {string} projectPath - Path to the Vue project
243
+ * @returns {string} - Formatted error message
244
+ */
245
+ export function formatMissingConfigError(projectPath) {
246
+ return `Missing Configuration
247
+ ────────────────────────────────────────────────────
248
+
249
+ Could not find config.ts in the Vue project.
250
+
251
+ Expected location: ${join(projectPath, 'src/config.ts')}
252
+
253
+ Required fields:
254
+ - slug: App identifier (used as folder name)
255
+ - appName: Display name for navigation menu
256
+ - siteUrl: Magentrix instance URL
257
+
258
+ Example config.ts:
259
+ export const config = {
260
+ slug: "my-app",
261
+ appName: "My Application",
262
+ siteUrl: "https://yourinstance.magentrix.com",
263
+ assets: []
264
+ }`;
265
+ }
266
+
267
+ /**
268
+ * Format config validation errors.
269
+ *
270
+ * @param {ReturnType<typeof readVueConfig>} config - Config read result
271
+ * @returns {string} - Formatted error message
272
+ */
273
+ export function formatConfigErrors(config) {
274
+ const lines = [
275
+ 'Invalid Configuration',
276
+ '────────────────────────────────────────────────────',
277
+ '',
278
+ `Config file: ${config.configPath || 'not found'}`,
279
+ ''
280
+ ];
281
+
282
+ if (config.errors.length > 0) {
283
+ lines.push('Issues:');
284
+ for (const error of config.errors) {
285
+ lines.push(` ✗ ${error}`);
286
+ }
287
+ }
288
+
289
+ lines.push('');
290
+ lines.push('Current values:');
291
+ lines.push(` slug: ${config.slug || '(missing)'}`);
292
+ lines.push(` appName: ${config.appName || '(missing)'}`);
293
+ lines.push(` siteUrl: ${config.siteUrl || '(missing)'}`);
294
+
295
+ return lines.join('\n');
296
+ }
@@ -0,0 +1,102 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import Config from '../config.js';
4
+ import { deleteApp } from '../magentrix/api/iris.js';
5
+
6
+ const config = new Config();
7
+
8
+ /**
9
+ * Delete an Iris app from the server and update cache.
10
+ * Does NOT delete local files or create backups - caller handles that.
11
+ *
12
+ * @param {string} instanceUrl - Magentrix instance URL
13
+ * @param {string} token - OAuth token
14
+ * @param {string} slug - App slug (folder name)
15
+ * @param {object} options - Options
16
+ * @param {boolean} options.updateCache - Whether to update base.json (default: true)
17
+ * @returns {Promise<{success: boolean, error: string | null, cleanedFromCache: boolean}>}
18
+ */
19
+ export async function deleteIrisAppFromServer(instanceUrl, token, slug, options = {}) {
20
+ const { updateCache = true } = options;
21
+ const result = {
22
+ success: false,
23
+ error: null,
24
+ cleanedFromCache: false
25
+ };
26
+
27
+ try {
28
+ // Delete from server
29
+ const deleteResult = await deleteApp(instanceUrl, token, slug);
30
+
31
+ if (!deleteResult.success) {
32
+ result.error = deleteResult.message || deleteResult.error || 'Unknown error';
33
+ return result;
34
+ }
35
+
36
+ result.success = true;
37
+
38
+ // Update cache if requested
39
+ if (updateCache) {
40
+ config.removeKey(`iris-app:${slug}`, { filename: 'base.json' });
41
+ result.cleanedFromCache = true;
42
+ }
43
+
44
+ return result;
45
+ } catch (error) {
46
+ // Check if this is a "not found" error (app already deleted on server)
47
+ const errorMessage = error?.message || String(error);
48
+ const errorLower = errorMessage.toLowerCase();
49
+ const isNotFound = errorLower.includes('404') ||
50
+ errorLower.includes('not found') ||
51
+ errorLower.includes('does not exist');
52
+
53
+ if (isNotFound) {
54
+ // App doesn't exist on server, but clean up local cache anyway
55
+ if (updateCache) {
56
+ config.removeKey(`iris-app:${slug}`, { filename: 'base.json' });
57
+ result.cleanedFromCache = true;
58
+ }
59
+ result.success = true;
60
+ result.error = 'App not found on server (already deleted)';
61
+ return result;
62
+ }
63
+
64
+ result.error = errorMessage;
65
+ return result;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Delete local Iris app files.
71
+ *
72
+ * @param {string} appPath - Absolute path to the app folder
73
+ * @returns {{success: boolean, existed: boolean, error: string | null, isPermissionError: boolean}}
74
+ */
75
+ export function deleteLocalIrisAppFiles(appPath) {
76
+ if (!fs.existsSync(appPath)) {
77
+ return {
78
+ success: true,
79
+ existed: false,
80
+ error: null,
81
+ isPermissionError: false
82
+ };
83
+ }
84
+
85
+ try {
86
+ fs.rmSync(appPath, { recursive: true, force: true });
87
+ return {
88
+ success: true,
89
+ existed: true,
90
+ error: null,
91
+ isPermissionError: false
92
+ };
93
+ } catch (error) {
94
+ const isPermissionError = error.code === 'EACCES' || error.code === 'EPERM';
95
+ return {
96
+ success: false,
97
+ existed: true,
98
+ error: error.message,
99
+ isPermissionError
100
+ };
101
+ }
102
+ }