@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,490 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, basename } from 'node:path';
|
|
3
|
+
import Config from '../config.js';
|
|
4
|
+
import { readVueConfig } from './config-reader.js';
|
|
5
|
+
|
|
6
|
+
const config = new Config();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get all linked Vue projects from global config.
|
|
10
|
+
*
|
|
11
|
+
* @returns {Array<{
|
|
12
|
+
* path: string,
|
|
13
|
+
* slug: string,
|
|
14
|
+
* appName: string,
|
|
15
|
+
* siteUrl: string | null,
|
|
16
|
+
* lastBuild: string | null
|
|
17
|
+
* }>}
|
|
18
|
+
*/
|
|
19
|
+
export function getLinkedProjects() {
|
|
20
|
+
const linkedProjects = config.read('linkedVueProjects', {
|
|
21
|
+
global: true // Store globally so projects persist across Magentrix workspaces
|
|
22
|
+
});
|
|
23
|
+
return linkedProjects || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Save linked projects to global config.
|
|
28
|
+
*
|
|
29
|
+
* @param {Array} projects - Array of linked projects
|
|
30
|
+
*/
|
|
31
|
+
function saveLinkedProjects(projects) {
|
|
32
|
+
config.save('linkedVueProjects', projects, {
|
|
33
|
+
global: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate a linked project and return its current status.
|
|
39
|
+
*
|
|
40
|
+
* @param {{path: string, slug: string, appName: string}} project - Project to validate
|
|
41
|
+
* @returns {{
|
|
42
|
+
* valid: boolean,
|
|
43
|
+
* exists: boolean,
|
|
44
|
+
* hasConfig: boolean,
|
|
45
|
+
* configValid: boolean,
|
|
46
|
+
* currentSlug: string | null,
|
|
47
|
+
* currentAppName: string | null,
|
|
48
|
+
* errors: string[]
|
|
49
|
+
* }}
|
|
50
|
+
*/
|
|
51
|
+
export function validateLinkedProject(project) {
|
|
52
|
+
const result = {
|
|
53
|
+
valid: false,
|
|
54
|
+
exists: false,
|
|
55
|
+
hasConfig: false,
|
|
56
|
+
configValid: false,
|
|
57
|
+
currentSlug: null,
|
|
58
|
+
currentAppName: null,
|
|
59
|
+
errors: []
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Check if path exists
|
|
63
|
+
if (!existsSync(project.path)) {
|
|
64
|
+
result.errors.push('Path no longer exists');
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
result.exists = true;
|
|
68
|
+
|
|
69
|
+
// Check if it's still a valid Vue project
|
|
70
|
+
const vueConfig = readVueConfig(project.path);
|
|
71
|
+
if (!vueConfig.found) {
|
|
72
|
+
result.errors.push('No config.ts found');
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
result.hasConfig = true;
|
|
76
|
+
|
|
77
|
+
// Check for config errors
|
|
78
|
+
if (vueConfig.errors.length > 0) {
|
|
79
|
+
result.errors.push(...vueConfig.errors);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
result.configValid = true;
|
|
83
|
+
|
|
84
|
+
// Store current values (might have changed)
|
|
85
|
+
result.currentSlug = vueConfig.slug;
|
|
86
|
+
result.currentAppName = vueConfig.appName;
|
|
87
|
+
result.valid = true;
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all linked projects with their current validation status.
|
|
94
|
+
*
|
|
95
|
+
* @returns {Array<{
|
|
96
|
+
* path: string,
|
|
97
|
+
* slug: string,
|
|
98
|
+
* appName: string,
|
|
99
|
+
* siteUrl: string | null,
|
|
100
|
+
* lastBuild: string | null,
|
|
101
|
+
* validation: {
|
|
102
|
+
* valid: boolean,
|
|
103
|
+
* exists: boolean,
|
|
104
|
+
* hasConfig: boolean,
|
|
105
|
+
* configValid: boolean,
|
|
106
|
+
* currentSlug: string | null,
|
|
107
|
+
* currentAppName: string | null,
|
|
108
|
+
* errors: string[]
|
|
109
|
+
* }
|
|
110
|
+
* }>}
|
|
111
|
+
*/
|
|
112
|
+
export function getLinkedProjectsWithStatus() {
|
|
113
|
+
const projects = getLinkedProjects();
|
|
114
|
+
|
|
115
|
+
return projects.map(project => ({
|
|
116
|
+
...project,
|
|
117
|
+
validation: validateLinkedProject(project)
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find a linked project by slug.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} slug - The app slug to find
|
|
125
|
+
* @returns {{
|
|
126
|
+
* path: string,
|
|
127
|
+
* slug: string,
|
|
128
|
+
* appName: string,
|
|
129
|
+
* siteUrl: string | null,
|
|
130
|
+
* lastBuild: string | null
|
|
131
|
+
* } | null}
|
|
132
|
+
*/
|
|
133
|
+
export function findLinkedProject(slug) {
|
|
134
|
+
const projects = getLinkedProjects();
|
|
135
|
+
return projects.find(p => p.slug === slug) || null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find a linked project by path.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} projectPath - The project path to find
|
|
142
|
+
* @returns {{
|
|
143
|
+
* path: string,
|
|
144
|
+
* slug: string,
|
|
145
|
+
* appName: string,
|
|
146
|
+
* siteUrl: string | null,
|
|
147
|
+
* lastBuild: string | null
|
|
148
|
+
* } | null}
|
|
149
|
+
*/
|
|
150
|
+
export function findLinkedProjectByPath(projectPath) {
|
|
151
|
+
const projects = getLinkedProjects();
|
|
152
|
+
const normalizedPath = resolve(projectPath);
|
|
153
|
+
return projects.find(p => resolve(p.path) === normalizedPath) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Link a Vue project to the CLI (stored globally).
|
|
158
|
+
*
|
|
159
|
+
* @param {string} projectPath - Path to the Vue project
|
|
160
|
+
* @returns {{
|
|
161
|
+
* success: boolean,
|
|
162
|
+
* project: {
|
|
163
|
+
* path: string,
|
|
164
|
+
* slug: string,
|
|
165
|
+
* appName: string,
|
|
166
|
+
* siteUrl: string | null,
|
|
167
|
+
* lastBuild: string | null
|
|
168
|
+
* } | null,
|
|
169
|
+
* error: string | null,
|
|
170
|
+
* updated: boolean
|
|
171
|
+
* }}
|
|
172
|
+
*/
|
|
173
|
+
export function linkVueProject(projectPath) {
|
|
174
|
+
const normalizedPath = resolve(projectPath);
|
|
175
|
+
|
|
176
|
+
// Validate the path exists
|
|
177
|
+
if (!existsSync(normalizedPath)) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
project: null,
|
|
181
|
+
error: `Path does not exist: ${normalizedPath}`,
|
|
182
|
+
updated: false
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if package.json exists (basic Vue project check)
|
|
187
|
+
const packageJsonPath = resolve(normalizedPath, 'package.json');
|
|
188
|
+
if (!existsSync(packageJsonPath)) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
project: null,
|
|
192
|
+
error: `Not a valid project directory (no package.json found): ${normalizedPath}`,
|
|
193
|
+
updated: false
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Read and validate Vue config
|
|
198
|
+
const vueConfig = readVueConfig(normalizedPath);
|
|
199
|
+
if (!vueConfig.found) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
project: null,
|
|
203
|
+
error: vueConfig.errors.join('\n'),
|
|
204
|
+
updated: false
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (vueConfig.errors.length > 0) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
project: null,
|
|
212
|
+
error: `Config validation errors:\n${vueConfig.errors.join('\n')}`,
|
|
213
|
+
updated: false
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if already linked
|
|
218
|
+
const existing = findLinkedProjectByPath(normalizedPath);
|
|
219
|
+
if (existing) {
|
|
220
|
+
// Update existing entry
|
|
221
|
+
const projects = getLinkedProjects();
|
|
222
|
+
const index = projects.findIndex(p => resolve(p.path) === normalizedPath);
|
|
223
|
+
projects[index] = {
|
|
224
|
+
...projects[index],
|
|
225
|
+
slug: vueConfig.slug,
|
|
226
|
+
appName: vueConfig.appName,
|
|
227
|
+
siteUrl: vueConfig.siteUrl
|
|
228
|
+
};
|
|
229
|
+
saveLinkedProjects(projects);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
project: projects[index],
|
|
234
|
+
error: null,
|
|
235
|
+
updated: true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check if slug conflicts with another linked project
|
|
240
|
+
const conflicting = findLinkedProject(vueConfig.slug);
|
|
241
|
+
if (conflicting) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
project: null,
|
|
245
|
+
error: `Slug '${vueConfig.slug}' is already used by another linked project: ${conflicting.path}`,
|
|
246
|
+
updated: false
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Create new linked project entry
|
|
251
|
+
const newProject = {
|
|
252
|
+
path: normalizedPath,
|
|
253
|
+
slug: vueConfig.slug,
|
|
254
|
+
appName: vueConfig.appName,
|
|
255
|
+
siteUrl: vueConfig.siteUrl,
|
|
256
|
+
lastBuild: null
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Add to linked projects
|
|
260
|
+
const projects = getLinkedProjects();
|
|
261
|
+
projects.push(newProject);
|
|
262
|
+
saveLinkedProjects(projects);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
project: newProject,
|
|
267
|
+
error: null,
|
|
268
|
+
updated: false
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Unlink a Vue project from the CLI.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} projectPathOrSlug - Path to the Vue project or its slug
|
|
276
|
+
* @returns {{
|
|
277
|
+
* success: boolean,
|
|
278
|
+
* project: {
|
|
279
|
+
* path: string,
|
|
280
|
+
* slug: string,
|
|
281
|
+
* appName: string
|
|
282
|
+
* } | null,
|
|
283
|
+
* error: string | null
|
|
284
|
+
* }}
|
|
285
|
+
*/
|
|
286
|
+
export function unlinkVueProject(projectPathOrSlug) {
|
|
287
|
+
const projects = getLinkedProjects();
|
|
288
|
+
|
|
289
|
+
// Try to find by path first
|
|
290
|
+
let index = projects.findIndex(p => resolve(p.path) === resolve(projectPathOrSlug));
|
|
291
|
+
|
|
292
|
+
// If not found by path, try by slug
|
|
293
|
+
if (index === -1) {
|
|
294
|
+
index = projects.findIndex(p => p.slug === projectPathOrSlug);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (index === -1) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
project: null,
|
|
301
|
+
error: `No linked project found for: ${projectPathOrSlug}`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remove from list
|
|
306
|
+
const [removed] = projects.splice(index, 1);
|
|
307
|
+
saveLinkedProjects(projects);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
project: removed,
|
|
312
|
+
error: null
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Remove all invalid (non-existent paths) linked projects.
|
|
318
|
+
*
|
|
319
|
+
* @returns {{
|
|
320
|
+
* removed: number,
|
|
321
|
+
* projects: Array<{path: string, slug: string, appName: string}>
|
|
322
|
+
* }}
|
|
323
|
+
*/
|
|
324
|
+
export function cleanupInvalidProjects() {
|
|
325
|
+
const projects = getLinkedProjects();
|
|
326
|
+
const validProjects = [];
|
|
327
|
+
const removedProjects = [];
|
|
328
|
+
|
|
329
|
+
for (const project of projects) {
|
|
330
|
+
if (existsSync(project.path)) {
|
|
331
|
+
validProjects.push(project);
|
|
332
|
+
} else {
|
|
333
|
+
removedProjects.push(project);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (removedProjects.length > 0) {
|
|
338
|
+
saveLinkedProjects(validProjects);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
removed: removedProjects.length,
|
|
343
|
+
projects: removedProjects
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Update the last build timestamp for a linked project.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} slug - The app slug
|
|
351
|
+
* @returns {boolean} - True if updated, false if not found
|
|
352
|
+
*/
|
|
353
|
+
export function updateLastBuild(slug) {
|
|
354
|
+
const projects = getLinkedProjects();
|
|
355
|
+
const index = projects.findIndex(p => p.slug === slug);
|
|
356
|
+
|
|
357
|
+
if (index === -1) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
projects[index].lastBuild = new Date().toISOString();
|
|
362
|
+
saveLinkedProjects(projects);
|
|
363
|
+
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Format linked projects list for display.
|
|
369
|
+
*
|
|
370
|
+
* @param {Array} projects - Array of linked projects (with or without validation)
|
|
371
|
+
* @returns {string} - Formatted string for display
|
|
372
|
+
*/
|
|
373
|
+
export function formatLinkedProjects(projects) {
|
|
374
|
+
if (!projects || projects.length === 0) {
|
|
375
|
+
return 'No Vue projects linked.\n\nUse `magentrix iris-link` to link a project.';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const lines = [
|
|
379
|
+
'Linked Vue Projects',
|
|
380
|
+
'────────────────────────────────────────────────────',
|
|
381
|
+
''
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
for (const project of projects) {
|
|
385
|
+
const validation = project.validation || validateLinkedProject(project);
|
|
386
|
+
|
|
387
|
+
// Status indicator
|
|
388
|
+
let statusIcon = '✓';
|
|
389
|
+
let statusColor = '\x1b[32m'; // green
|
|
390
|
+
if (!validation.valid) {
|
|
391
|
+
statusIcon = '⚠';
|
|
392
|
+
statusColor = '\x1b[33m'; // yellow
|
|
393
|
+
}
|
|
394
|
+
if (!validation.exists) {
|
|
395
|
+
statusIcon = '✗';
|
|
396
|
+
statusColor = '\x1b[31m'; // red
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const displayName = validation.currentAppName || project.appName;
|
|
400
|
+
const displaySlug = validation.currentSlug || project.slug;
|
|
401
|
+
|
|
402
|
+
lines.push(` ${statusColor}${statusIcon}\x1b[0m ${displayName} (${displaySlug})`);
|
|
403
|
+
lines.push(` Path: ${project.path}`);
|
|
404
|
+
|
|
405
|
+
if (project.siteUrl) {
|
|
406
|
+
lines.push(` Site: ${project.siteUrl}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (project.lastBuild) {
|
|
410
|
+
const date = new Date(project.lastBuild);
|
|
411
|
+
lines.push(` Last build: ${date.toLocaleString()}`);
|
|
412
|
+
} else {
|
|
413
|
+
lines.push(` Last build: Never`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Show validation errors
|
|
417
|
+
if (!validation.valid && validation.errors.length > 0) {
|
|
418
|
+
lines.push(` \x1b[33mWarning: ${validation.errors.join(', ')}\x1b[0m`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
lines.push('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Build choices array for inquirer select prompt.
|
|
429
|
+
*
|
|
430
|
+
* @param {Object} options - Options for building choices
|
|
431
|
+
* @param {boolean} options.includeManual - Include "Enter path manually" option
|
|
432
|
+
* @param {boolean} options.includeCancel - Include "Cancel" option
|
|
433
|
+
* @param {boolean} options.showInvalid - Show invalid projects (disabled)
|
|
434
|
+
* @returns {Array} - Choices array for select prompt
|
|
435
|
+
*/
|
|
436
|
+
export function buildProjectChoices(options = {}) {
|
|
437
|
+
const {
|
|
438
|
+
includeManual = true,
|
|
439
|
+
includeCancel = true,
|
|
440
|
+
showInvalid = true
|
|
441
|
+
} = options;
|
|
442
|
+
|
|
443
|
+
const projectsWithStatus = getLinkedProjectsWithStatus();
|
|
444
|
+
const choices = [];
|
|
445
|
+
|
|
446
|
+
// Add valid projects first
|
|
447
|
+
for (const project of projectsWithStatus) {
|
|
448
|
+
if (project.validation.valid) {
|
|
449
|
+
const displayName = project.validation.currentAppName || project.appName;
|
|
450
|
+
const displaySlug = project.validation.currentSlug || project.slug;
|
|
451
|
+
|
|
452
|
+
choices.push({
|
|
453
|
+
name: `${displayName} (${displaySlug})`,
|
|
454
|
+
value: { type: 'linked', path: project.path, slug: displaySlug }
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Add invalid projects (disabled) if requested
|
|
460
|
+
if (showInvalid) {
|
|
461
|
+
for (const project of projectsWithStatus) {
|
|
462
|
+
if (!project.validation.valid) {
|
|
463
|
+
const errorMsg = project.validation.errors[0] || 'Invalid';
|
|
464
|
+
choices.push({
|
|
465
|
+
name: `⚠ ${project.appName} (${project.slug})`,
|
|
466
|
+
value: { type: 'invalid', path: project.path },
|
|
467
|
+
disabled: errorMsg
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Add manual entry option
|
|
474
|
+
if (includeManual) {
|
|
475
|
+
choices.push({
|
|
476
|
+
name: 'Enter path manually',
|
|
477
|
+
value: { type: 'manual' }
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add cancel option
|
|
482
|
+
if (includeCancel) {
|
|
483
|
+
choices.push({
|
|
484
|
+
name: 'Cancel',
|
|
485
|
+
value: { type: 'cancel' }
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return choices;
|
|
490
|
+
}
|