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