@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.
- package/README.md +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +183 -9
- package/actions/pull.js +107 -4
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +10 -2
- package/vars/global.js +1 -0
|
@@ -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
|
+
}
|