slicejs-cli 3.3.0 → 3.4.1

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.
Files changed (46) hide show
  1. package/AGENTS.md +247 -0
  2. package/LICENSE +21 -21
  3. package/client.js +663 -626
  4. package/commands/Print.js +163 -167
  5. package/commands/Validations.js +92 -103
  6. package/commands/build/build.js +40 -40
  7. package/commands/buildProduction/buildProduction.js +576 -579
  8. package/commands/bundle/bundle.js +234 -235
  9. package/commands/createComponent/VisualComponentTemplate.js +55 -55
  10. package/commands/createComponent/createComponent.js +124 -126
  11. package/commands/deleteComponent/deleteComponent.js +77 -77
  12. package/commands/doctor/doctor.js +366 -369
  13. package/commands/getComponent/getComponent.js +684 -747
  14. package/commands/init/init.js +269 -261
  15. package/commands/listComponents/listComponents.js +172 -175
  16. package/commands/startServer/startServer.js +261 -264
  17. package/commands/startServer/watchServer.js +79 -79
  18. package/commands/types/types.js +69 -27
  19. package/commands/utils/LocalCliDelegation.js +53 -53
  20. package/commands/utils/PathHelper.js +75 -68
  21. package/commands/utils/VersionChecker.js +167 -167
  22. package/commands/utils/bundling/BundleGenerator.js +2292 -2292
  23. package/commands/utils/bundling/DependencyAnalyzer.js +925 -933
  24. package/commands/utils/loadConfig.js +31 -0
  25. package/commands/utils/updateManager.js +452 -453
  26. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
  27. package/package.json +58 -46
  28. package/post.js +66 -65
  29. package/tests/bundle-generator.test.js +691 -708
  30. package/tests/bundle-v2-register-output.test.js +470 -470
  31. package/tests/client-launcher-contract.test.js +211 -211
  32. package/tests/client-update-flow-contract.test.js +272 -272
  33. package/tests/component-registry-parse.test.js +34 -0
  34. package/tests/dependency-analyzer.test.js +24 -24
  35. package/tests/fixtures/components.js +8 -0
  36. package/tests/fixtures/sliceConfig.json +74 -0
  37. package/tests/getcomponent.test.js +407 -0
  38. package/tests/helpers/setup.js +97 -0
  39. package/tests/init-command-contract.test.js +46 -0
  40. package/tests/local-cli-delegation.test.js +81 -79
  41. package/tests/path-helper.test.js +206 -0
  42. package/tests/types-breakage.test.js +491 -0
  43. package/tests/types-generator-errors.test.js +361 -0
  44. package/tests/types-generator.test.js +172 -184
  45. package/tests/update-manager-notifications.test.js +88 -88
  46. package/.github/workflows/docs-render-cicd.yml +0 -65
@@ -1,747 +1,684 @@
1
- // commands/getComponent/getComponent.js
2
-
3
- import fs from "fs-extra";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- import inquirer from "inquirer";
7
- import validations from "../Validations.js";
8
- import Print from "../Print.js";
9
- import { getConfigPath, getComponentsJsPath, getPath } from "../utils/PathHelper.js";
10
- import ora from "ora";
11
-
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
-
14
- // Base URL del repositorio de documentación de Slice.js
15
- const DOCS_REPO_BASE_URL = 'https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components';
16
- const COMPONENTS_REGISTRY_URL = 'https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components/components.js';
17
-
18
- /**
19
- * Carga la configuración desde sliceConfig.json
20
- * @returns {object} - Objeto de configuración
21
- */
22
- const loadConfig = () => {
23
- try {
24
- const configPath = getConfigPath(import.meta.url);
25
- if (!fs.existsSync(configPath)) {
26
- throw new Error('sliceConfig.json not found in src folder');
27
- }
28
- const rawData = fs.readFileSync(configPath, 'utf-8');
29
- return JSON.parse(rawData);
30
- } catch (error) {
31
- console.error(`Error loading configuration: ${error.message}`);
32
- return null;
33
- }
34
- };
35
-
36
- class ComponentRegistry {
37
- constructor() {
38
- this.componentsRegistry = null;
39
- this.config = loadConfig();
40
- }
41
-
42
- async loadRegistry() {
43
- Print.info('Loading component registry from official repository...');
44
-
45
- try {
46
- const response = await fetch(COMPONENTS_REGISTRY_URL);
47
-
48
- if (!response.ok) {
49
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
50
- }
51
-
52
- const content = await response.text();
53
-
54
- // Parse the components.js file content
55
- const match = content.match(/const components = ({[\s\S]*?});/);
56
- if (!match) {
57
- throw new Error('Invalid components.js format from repository');
58
- }
59
-
60
- const allComponents = eval('(' + match[1] + ')');
61
-
62
- // ✅ NUEVO: FILTRAR solo componentes Visual y Service
63
- this.componentsRegistry = this.filterOfficialComponents(allComponents);
64
-
65
- Print.success('Component registry loaded successfully');
66
-
67
- } catch (error) {
68
- Print.error(`Loading component registry: ${error.message}`);
69
- Print.info('Check your internet connection and repository accessibility');
70
- throw error;
71
- }
72
- }
73
-
74
- /**
75
- * Filtra el registry para incluir SOLO componentes de categorías Visual y Service
76
- * Excluye AppComponents y cualquier otra categoría
77
- * @param {object} allComponents - Objeto con todos los componentes del registry
78
- * @returns {object} - Objeto filtrado solo con Visual y Service
79
- */
80
- filterOfficialComponents(allComponents) {
81
- const filtered = {};
82
- let excludedCount = 0;
83
-
84
- Object.entries(allComponents).forEach(([name, category]) => {
85
- // Solo incluir componentes de categoría Visual o Service
86
- if (category === 'Visual' || category === 'Service') {
87
- filtered[name] = category;
88
- } else {
89
- excludedCount++;
90
- }
91
- });
92
-
93
- if (excludedCount > 0) {
94
- Print.info(`Filtered out ${excludedCount} non-Visual/Service components from registry`);
95
- }
96
-
97
- return filtered;
98
- }
99
-
100
- async getLocalComponents() {
101
- try {
102
- const componentsPath = getComponentsJsPath(import.meta.url);
103
-
104
- if (!await fs.pathExists(componentsPath)) {
105
- return {};
106
- }
107
-
108
- const content = await fs.readFile(componentsPath, 'utf8');
109
- const match = content.match(/const components = ({[\s\S]*?});/);
110
-
111
- if (!match) {
112
- return {};
113
- }
114
-
115
- return eval('(' + match[1] + ')');
116
- } catch (error) {
117
- Print.warning('⚠️ No se pudo leer el registro local de componentes');
118
- return {};
119
- }
120
- }
121
-
122
- async findUpdatableComponents() {
123
- const localComponents = await this.getLocalComponents();
124
- const updatableComponents = [];
125
-
126
- Object.entries(localComponents).forEach(([name, category]) => {
127
- // Check if component exists in remote registry
128
- if (this.componentsRegistry[name] && this.componentsRegistry[name] === category) {
129
- // Check if local component directory exists using dynamic paths
130
- const categoryPath = validations.getCategoryPath(category);
131
-
132
- // CORREGIDO: Usar 4 niveles para compatibilidad con node_modules
133
- const isProduction = this.config?.production?.enabled === true;
134
- const folderSuffix = isProduction ? 'dist' : 'src';
135
- const componentPath = getPath(import.meta.url, folderSuffix, categoryPath, name);
136
-
137
- if (fs.pathExistsSync(componentPath)) {
138
- updatableComponents.push({
139
- name,
140
- category,
141
- path: componentPath
142
- });
143
- }
144
- }
145
- });
146
-
147
- return updatableComponents;
148
- }
149
-
150
- getAvailableComponents(category = null) {
151
- if (!this.componentsRegistry) return {};
152
-
153
- const components = {};
154
- Object.entries(this.componentsRegistry).forEach(([name, componentCategory]) => {
155
- if (!category || componentCategory === category) {
156
- // CORREGIDO: Componentes especiales que no necesitan todos los archivos
157
- let files;
158
- if (componentCategory === 'Visual') {
159
- // Componentes de routing lógico solo necesitan JS
160
- if (['Route', 'MultiRoute', 'NotFound'].includes(name)) {
161
- files = [`${name}.js`];
162
- } else {
163
- // Componentes visuales normales necesitan JS, HTML, CSS
164
- files = [`${name}.js`, `${name}.html`, `${name}.css`];
165
- }
166
- } else {
167
- // Service components solo necesitan JS
168
- files = [`${name}.js`];
169
- }
170
-
171
- components[name] = {
172
- name,
173
- category: componentCategory,
174
- files: files
175
- };
176
- }
177
- });
178
-
179
- return components;
180
- }
181
-
182
-
183
- async downloadComponentFiles(componentName, category, targetPath) {
184
- const component = this.getAvailableComponents(category)[componentName];
185
-
186
- if (!component) {
187
- throw new Error(`Component ${componentName} not found in ${category} category`);
188
- }
189
-
190
- const downloadedFiles = [];
191
- const failedFiles = [];
192
- const total = component.files.length;
193
- let done = 0;
194
- const spinner = ora(`Downloading ${componentName} 0/${total}`).start();
195
- const fetchWithRetry = async (url, retries = 3, baseDelay = 500) => {
196
- for (let attempt = 0; attempt <= retries; attempt++) {
197
- try {
198
- const response = await fetch(url);
199
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
200
- return await response.text();
201
- } catch (e) {
202
- if (attempt === retries) throw e;
203
- const delay = baseDelay * Math.pow(2, attempt);
204
- await new Promise(r => setTimeout(r, delay));
205
- }
206
- }
207
- };
208
- const worker = async (fileName) => {
209
- const url = `${DOCS_REPO_BASE_URL}/${category}/${componentName}/${fileName}`;
210
- const localPath = path.join(targetPath, fileName);
211
- try {
212
- const content = await fetchWithRetry(url);
213
- await fs.writeFile(localPath, content, 'utf8');
214
- downloadedFiles.push(fileName);
215
- Print.downloadSuccess(fileName);
216
- } catch (error) {
217
- Print.downloadError(fileName, error.message);
218
- failedFiles.push(fileName);
219
- } finally {
220
- done += 1;
221
- spinner.text = `Downloading ${componentName} ${done}/${total}`;
222
- }
223
- };
224
- const runConcurrent = async (items, concurrency = 3) => {
225
- let index = 0;
226
- const runners = Array(Math.min(concurrency, items.length)).fill(0).map(async () => {
227
- while (true) {
228
- const i = index++;
229
- if (i >= items.length) break;
230
- await worker(items[i]);
231
- }
232
- });
233
- await Promise.all(runners);
234
- };
235
- await runConcurrent(component.files, 3);
236
- spinner.stop();
237
-
238
- // ✅ NUEVO: Solo lanzar error si NO se descargó el archivo principal (.js)
239
- const mainFileDownloaded = downloadedFiles.some(file => file.endsWith('.js'));
240
-
241
- if (!mainFileDownloaded) {
242
- throw new Error(`Failed to download main component file (${componentName}.js)`);
243
- }
244
-
245
- // ✅ ADVERTENCIA: Informar sobre archivos que fallaron (pero no detener el proceso)
246
- if (failedFiles.length > 0) {
247
- Print.warning(`Some files couldn't be downloaded: ${failedFiles.join(', ')}`);
248
- Print.info('Component installed with available files');
249
- }
250
-
251
- return downloadedFiles;
252
- }
253
-
254
- async updateLocalRegistrySafe(componentName, category) {
255
- const componentsPath = path.join(__dirname, '../../../../src/Components/components.js');
256
- try {
257
- if (!await fs.pathExists(componentsPath)) {
258
- const dir = path.dirname(componentsPath);
259
- await fs.ensureDir(dir);
260
- const initial = `const components = {};\n\nexport default components;\n`;
261
- await fs.writeFile(componentsPath, initial, 'utf8');
262
- }
263
- const content = await fs.readFile(componentsPath, 'utf8');
264
- const match = content.match(/const components = ({[\s\S]*?});/);
265
- if (!match) throw new Error('Invalid components.js format in local project');
266
- const componentsObj = eval('(' + match[1] + ')');
267
- if (!componentsObj[componentName]) {
268
- componentsObj[componentName] = category;
269
- const sorted = Object.keys(componentsObj)
270
- .sort()
271
- .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
272
- const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
273
- await fs.writeFile(componentsPath, newContent, 'utf8');
274
- Print.registryUpdate(`Registered ${componentName} in local components.js`);
275
- } else {
276
- Print.info(`${componentName} already exists in local registry`);
277
- }
278
- } catch (error) {
279
- Print.error(`Updating local components.js: ${error.message}`);
280
- throw error;
281
- }
282
- }
283
-
284
- async updateLocalRegistry(componentName, category) {
285
- // CORREGIDO: Usar 4 niveles para compatibilidad con node_modules
286
- const componentsPath = path.join(__dirname, '../../../../src/Components/components.js');
287
-
288
- try {
289
- let content = await fs.readFile(componentsPath, 'utf8');
290
-
291
- // Parse existing components
292
- const componentsMatch = content.match(/const components = ({[\s\S]*?});/);
293
- if (!componentsMatch) {
294
- throw new Error('Invalid components.js format in local project');
295
- }
296
-
297
- const componentsObj = eval('(' + componentsMatch[1] + ')');
298
-
299
- // Add new component if it doesn't exist
300
- if (!componentsObj[componentName]) {
301
- componentsObj[componentName] = category;
302
-
303
- // Generate new content
304
- const sortedComponents = Object.keys(componentsObj)
305
- .sort()
306
- .reduce((obj, key) => {
307
- obj[key] = componentsObj[key];
308
- return obj;
309
- }, {});
310
-
311
- const newComponentsString = JSON.stringify(sortedComponents, null, 2)
312
- .replace(/"/g, '"')
313
- .replace(/: "/g, ': "')
314
- .replace(/",\n/g, '",\n');
315
-
316
- const newContent = `const components = ${newComponentsString}; export default components;`;
317
-
318
- await fs.writeFile(componentsPath, newContent, 'utf8');
319
- Print.registryUpdate(`Registered ${componentName} in local components.js`);
320
- } else {
321
- Print.info(`${componentName} already exists in local registry`);
322
- }
323
-
324
- } catch (error) {
325
- Print.error(`Updating local components.js: ${error.message}`);
326
- throw error;
327
- }
328
- }
329
-
330
- async installComponent(componentName, category, force = false) {
331
- const availableComponents = this.getAvailableComponents(category);
332
-
333
- if (!availableComponents[componentName]) {
334
- throw new Error(`Component '${componentName}' not found in category '${category}' in the official repository`);
335
- }
336
-
337
- // MEJORADO: Detectar si validations tiene acceso a la configuración
338
- let categoryPath;
339
- const hasValidConfig = validations.config &&
340
- validations.config.paths &&
341
- validations.config.paths.components &&
342
- validations.config.paths.components[category];
343
-
344
- if (hasValidConfig) {
345
- categoryPath = validations.getCategoryPath(category);
346
- } else {
347
- if (category === 'Visual') {
348
- categoryPath = 'Components/Visual';
349
- } else if (category === 'Service') {
350
- categoryPath = 'Components/Service';
351
- } else {
352
- throw new Error(`Unknown category: ${category}`);
353
- }
354
- }
355
-
356
- const isProduction = this.config?.production?.enabled === true;
357
- const folderSuffix = isProduction ? 'dist' : 'src';
358
-
359
- const cleanCategoryPath = categoryPath ? categoryPath.replace(/^[/\\]+/, '') : '';
360
- const targetPath = getPath(import.meta.url, folderSuffix, cleanCategoryPath, componentName);
361
-
362
-
363
-
364
- // Check if component already exists
365
- if (await fs.pathExists(targetPath) && !force) {
366
- const { overwrite } = await inquirer.prompt([
367
- {
368
- type: 'confirm',
369
- name: 'overwrite',
370
- message: `The component '${componentName}' already exists locally. Overwrite with the repository version?`,
371
- default: false
372
- }
373
- ]);
374
-
375
- if (!overwrite) {
376
- Print.info('Installation cancelled by user');
377
- return false;
378
- }
379
- }
380
-
381
- try {
382
- // Create component directory
383
- await fs.ensureDir(targetPath);
384
-
385
- // Download component files
386
- const downloadedFiles = await this.downloadComponentFiles(componentName, category, targetPath);
387
-
388
- await this.updateLocalRegistrySafe(componentName, category);
389
-
390
- Print.success(`${componentName} installed successfully from official repository!`);
391
- console.log(`📁 Location: ${folderSuffix}/${categoryPath}/${componentName}/`);
392
- console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
393
-
394
- return true;
395
-
396
- } catch (error) {
397
- Print.error(`Error installing ${componentName}: ${error.message}`);
398
-
399
- // MEJORADO: Solo borrar si el archivo principal (.js) no existe
400
- const mainFilePath = path.join(targetPath, `${componentName}.js`);
401
- const mainFileExists = await fs.pathExists(mainFilePath);
402
-
403
- if (!mainFileExists && await fs.pathExists(targetPath)) {
404
- // Solo limpiar si no se instaló el archivo principal
405
- await fs.remove(targetPath);
406
- Print.info('Cleaned up failed installation');
407
- } else if (mainFileExists) {
408
- Print.warning('Component partially installed - main file exists');
409
- }
410
-
411
- throw error;
412
- }
413
- }
414
-
415
- async installMultipleComponents(componentNames, category = 'Visual', force = false) {
416
- const results = [];
417
- Print.info(`Getting ${componentNames.length} ${category} components from official repository...`);
418
- const total = componentNames.length;
419
- let done = 0;
420
- const spinner = ora(`Installing 0/${total}`).start();
421
- const worker = async (componentName) => {
422
- try {
423
- const result = await this.installComponent(componentName, category, force);
424
- results.push({ name: componentName, success: result });
425
- } catch (error) {
426
- Print.componentError(componentName, 'getting', error.message);
427
- results.push({ name: componentName, success: false, error: error.message });
428
- } finally {
429
- done += 1;
430
- spinner.text = `Installing ${done}/${total}`;
431
- }
432
- };
433
- const runConcurrent = async (items, concurrency = 3) => {
434
- let index = 0;
435
- const runners = Array(Math.min(concurrency, items.length)).fill(0).map(async () => {
436
- while (true) {
437
- const i = index++;
438
- if (i >= items.length) break;
439
- await worker(items[i]);
440
- }
441
- });
442
- await Promise.all(runners);
443
- };
444
- await runConcurrent(componentNames, 3);
445
- spinner.stop();
446
-
447
- // Summary
448
- const successful = results.filter(r => r.success).length;
449
- const failed = results.filter(r => !r.success).length;
450
-
451
- Print.newLine();
452
- Print.summary(successful, failed, componentNames.length);
453
-
454
- return results;
455
- }
456
-
457
- async updateAllComponents(force = false) {
458
- Print.info('Looking for updatable Visual components...');
459
-
460
- const allUpdatableComponents = await this.findUpdatableComponents();
461
-
462
- // NUEVO: Filtrar solo componentes Visual
463
- const updatableComponents = allUpdatableComponents.filter(comp => comp.category === 'Visual');
464
-
465
- if (updatableComponents.length === 0) {
466
- Print.info('No local Visual components found that match the official repository');
467
- Print.info('Use "slice browse" to see available components');
468
- return true;
469
- }
470
-
471
- // Mostrar estadísticas si hay componentes Service que no se sincronizarán
472
- const serviceComponents = allUpdatableComponents.filter(comp => comp.category === 'Service');
473
- if (serviceComponents.length > 0) {
474
- Print.info(`Found ${serviceComponents.length} Service components (skipped - sync only affects Visual components)`);
475
- }
476
-
477
- Print.newLine();
478
- Print.subtitle(`Found ${updatableComponents.length} updatable Visual components:`);
479
- Print.newLine();
480
- updatableComponents.forEach(comp => {
481
- console.log(`🎨 ${comp.name} (${comp.category})`);
482
- });
483
-
484
- if (!force) {
485
- const { confirmUpdate } = await inquirer.prompt([
486
- {
487
- type: 'confirm',
488
- name: 'confirmUpdate',
489
- message: `Do you want to update these Visual components to the repository versions?`,
490
- default: true
491
- }
492
- ]);
493
-
494
- if (!confirmUpdate) {
495
- Print.info('Update cancelled by user');
496
- return false;
497
- }
498
- }
499
-
500
- // SIMPLIFICADO: Solo actualizar componentes Visual
501
- const visualComponentNames = updatableComponents.map(c => c.name);
502
-
503
- Print.info(`Updating ${visualComponentNames.length} Visual components...`);
504
- const results = await this.installMultipleComponents(visualComponentNames, 'Visual', true);
505
-
506
- // Final summary
507
- const totalSuccessful = results.filter(r => r.success).length;
508
- const totalFailed = results.filter(r => !r.success).length;
509
-
510
- Print.newLine();
511
- Print.title('Visual Components Sync Summary');
512
- Print.success(`Visual components updated: ${totalSuccessful}`);
513
-
514
- if (totalFailed > 0) {
515
- Print.error(`Visual components failed: ${totalFailed}`);
516
- } else {
517
- Print.success('All your Visual components are now updated to the latest official versions!');
518
- }
519
-
520
- // Información adicional sobre Service components
521
- if (serviceComponents.length > 0) {
522
- Print.newLine();
523
- Print.info(`Note: ${serviceComponents.length} Service components were found but not updated`);
524
- Print.info('Service components maintain manual versioning - update them individually if needed');
525
- Print.commandExample('Update Service component manually', 'slice get FetchManager --service --force');
526
- }
527
-
528
- return totalFailed === 0;
529
- }
530
-
531
- displayAvailableComponents() {
532
- if (!this.componentsRegistry) {
533
- Print.error('❌ Could not load component registry');
534
- return;
535
- }
536
-
537
- console.log('\n📚 Available components in the official Slice.js repository:\n');
538
-
539
- const visualComponents = this.getAvailableComponents('Visual');
540
- const serviceComponents = this.getAvailableComponents('Service');
541
-
542
- // ✅ SIMPLIFICADO: Solo mostrar nombres sin descripciones
543
- Print.info('🎨 Visual Components (UI):');
544
- Object.keys(visualComponents).forEach(name => {
545
- const files = visualComponents[name].files;
546
- const fileIcons = files.map(file => {
547
- if (file.endsWith('.js')) return '📜';
548
- if (file.endsWith('.html')) return '🌐';
549
- if (file.endsWith('.css')) return '🎨';
550
- return '📄';
551
- }).join(' ');
552
- console.log(` • ${name} ${fileIcons}`);
553
- });
554
-
555
- Print.info('\n⚙️ Service Components (Logic):');
556
- Object.keys(serviceComponents).forEach(name => {
557
- console.log(` • ${name} 📜`);
558
- });
559
-
560
- Print.newLine();
561
- Print.info(`Total: ${Object.keys(visualComponents).length} Visual + ${Object.keys(serviceComponents).length} Service components`);
562
-
563
- console.log(`\n💡 Usage examples:`);
564
- console.log(`slice get Button Card Input # Install Visual components`);
565
- console.log(`slice get FetchManager --service # Install Service component`);
566
- console.log(`slice sync # Sync Visual components`);
567
- }
568
-
569
- async interactiveInstall() {
570
- const { componentType } = await inquirer.prompt([
571
- {
572
- type: 'list',
573
- name: 'componentType',
574
- message: 'Select the type of component to install from the repository:',
575
- choices: [
576
- { name: '🎨 Visual Components (UI)', value: 'Visual' },
577
- { name: '⚙️ Service Components (Logic)', value: 'Service' }
578
- ]
579
- }
580
- ]);
581
-
582
- const availableComponents = this.getAvailableComponents(componentType);
583
- const componentChoices = Object.keys(availableComponents).map(name => ({
584
- name: name,
585
- value: name
586
- }));
587
-
588
- if (componentType === 'Visual') {
589
- const { installMode } = await inquirer.prompt([
590
- {
591
- type: 'list',
592
- name: 'installMode',
593
- message: 'How do you want to install Visual components?',
594
- choices: [
595
- { name: 'Get one', value: 'single' },
596
- { name: 'Get multiple', value: 'multiple' }
597
- ]
598
- }
599
- ]);
600
-
601
- if (installMode === 'multiple') {
602
- const { selectedComponents } = await inquirer.prompt([
603
- {
604
- type: 'checkbox',
605
- name: 'selectedComponents',
606
- message: 'Select Visual components to install from the repository:',
607
- choices: componentChoices,
608
- validate: (input) => {
609
- if (input.length === 0) {
610
- return 'You must select at least one component';
611
- }
612
- return true;
613
- }
614
- }
615
- ]);
616
-
617
- await this.installMultipleComponents(selectedComponents, componentType);
618
- } else {
619
- const { selectedComponent } = await inquirer.prompt([
620
- {
621
- type: 'list',
622
- name: 'selectedComponent',
623
- message: 'Select a Visual component:',
624
- choices: componentChoices
625
- }
626
- ]);
627
-
628
- await this.installComponent(selectedComponent, componentType);
629
- }
630
- } else {
631
- const { selectedComponent } = await inquirer.prompt([
632
- {
633
- type: 'list',
634
- name: 'selectedComponent',
635
- message: 'Select a Service component:',
636
- choices: componentChoices
637
- }
638
- ]);
639
-
640
- await this.installComponent(selectedComponent, componentType);
641
- }
642
- }
643
-
644
- findComponentInRegistry(componentName) {
645
- if (!this.componentsRegistry) return null;
646
-
647
- const normalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
648
-
649
- if (this.componentsRegistry[normalizedName]) {
650
- return {
651
- name: normalizedName,
652
- category: this.componentsRegistry[normalizedName]
653
- };
654
- }
655
-
656
- return null;
657
- }
658
- }
659
-
660
- // Main get function
661
- async function getComponents(componentNames = [], options = {}) {
662
- const registry = new ComponentRegistry();
663
-
664
- try {
665
- await registry.loadRegistry();
666
- } catch (error) {
667
- Print.error('Could not load component registry from official repository');
668
- Print.info('Check your internet connection and try again');
669
- return false;
670
- }
671
-
672
- // Interactive mode if no components specified
673
- if (!componentNames || componentNames.length === 0) {
674
- await registry.interactiveInstall();
675
- return true;
676
- }
677
-
678
- // Determine category
679
- const category = options.service ? 'Service' : 'Visual';
680
-
681
- if (componentNames.length === 1) {
682
- // Single component install
683
- const componentInfo = registry.findComponentInRegistry(componentNames[0]);
684
-
685
- if (!componentInfo) {
686
- Print.error(`Component '${componentNames[0]}' not found in official repository`);
687
- Print.commandExample('View available components', 'slice browse');
688
- return false;
689
- }
690
-
691
- // Use the category from registry unless Service is explicitly requested
692
- const actualCategory = options.service ? 'Service' : componentInfo.category;
693
-
694
- try {
695
- await registry.installComponent(componentInfo.name, actualCategory, options.force);
696
- return true;
697
- } catch (error) {
698
- Print.error(`${error.message}`);
699
- return false;
700
- }
701
- } else {
702
- // Multiple components install
703
- const normalizedComponents = componentNames.map(name =>
704
- name.charAt(0).toUpperCase() + name.slice(1)
705
- );
706
-
707
- try {
708
- await registry.installMultipleComponents(normalizedComponents, category, options.force);
709
- return true;
710
- } catch (error) {
711
- Print.error(`${error.message}`);
712
- return false;
713
- }
714
- }
715
- }
716
-
717
- // List components function
718
- async function listComponents() {
719
- const registry = new ComponentRegistry();
720
-
721
- try {
722
- await registry.loadRegistry();
723
- registry.displayAvailableComponents();
724
- return true;
725
- } catch (error) {
726
- Print.error('Could not load component registry from official repository');
727
- Print.info('Check your internet connection and try again');
728
- return false;
729
- }
730
- }
731
-
732
- // Sync components function
733
- async function syncComponents(options = {}) {
734
- const registry = new ComponentRegistry();
735
-
736
- try {
737
- await registry.loadRegistry();
738
- return await registry.updateAllComponents(options.force);
739
- } catch (error) {
740
- Print.error('Could not load component registry from official repository');
741
- Print.info('Check your internet connection and try again');
742
- return false;
743
- }
744
- }
745
-
746
- export default getComponents;
747
- export { listComponents, syncComponents, ComponentRegistry };
1
+ // commands/getComponent/getComponent.js
2
+
3
+ import fs from "fs-extra";
4
+ import path from "path";
5
+ import inquirer from "inquirer";
6
+ import validations from "../Validations.js";
7
+ import Print from "../Print.js";
8
+ import { getComponentsJsPath, getPath } from "../utils/PathHelper.js";
9
+ import { loadConfig as sharedLoadConfig } from "../utils/loadConfig.js";
10
+ import ora from "ora";
11
+
12
+ // Base URL of the Slice.js documentation repository
13
+ const DOCS_REPO_BASE_URL = 'https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components';
14
+ const COMPONENTS_REGISTRY_URL = 'https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components/components.js';
15
+
16
+ /**
17
+ * Loads configuration from sliceConfig.json
18
+ * @returns {object} - Configuration object
19
+ */
20
+ const loadConfig = () => sharedLoadConfig(import.meta.url);
21
+
22
+ const fetchWithRetry = async (url, retries = 3, baseDelay = 500) => {
23
+ for (let attempt = 0; attempt <= retries; attempt++) {
24
+ try {
25
+ const response = await fetch(url);
26
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
27
+ return await response.text();
28
+ } catch (e) {
29
+ if (attempt === retries) throw e;
30
+ const delay = baseDelay * Math.pow(2, attempt);
31
+ await new Promise(r => setTimeout(r, delay));
32
+ }
33
+ }
34
+ };
35
+
36
+ const runConcurrent = async (items, worker, concurrency = 3) => {
37
+ let index = 0;
38
+ const runners = Array(Math.min(concurrency, items.length)).fill(0).map(async () => {
39
+ while (true) {
40
+ const i = index++;
41
+ if (i >= items.length) break;
42
+ await worker(items[i]);
43
+ }
44
+ });
45
+ await Promise.all(runners);
46
+ };
47
+
48
+ class ComponentRegistry {
49
+ constructor() {
50
+ this.componentsRegistry = null;
51
+ this.config = null;
52
+ this._configPromise = null;
53
+ }
54
+
55
+ async _ensureConfig() {
56
+ if (!this.config && !this._configPromise) {
57
+ this._configPromise = loadConfig();
58
+ }
59
+ if (this._configPromise) {
60
+ this.config = await this._configPromise;
61
+ }
62
+ }
63
+
64
+ async loadRegistry() {
65
+ await this._ensureConfig();
66
+ Print.info('Loading component registry from official repository...');
67
+
68
+ try {
69
+ const response = await fetch(COMPONENTS_REGISTRY_URL);
70
+
71
+ if (!response.ok) {
72
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
73
+ }
74
+
75
+ const content = await response.text();
76
+
77
+ // Parse the components.js file content
78
+ const match = content.match(/const components = ({[\s\S]*?});/);
79
+ if (!match) {
80
+ throw new Error('Invalid components.js format from repository');
81
+ }
82
+
83
+ const allComponents = JSON.parse(match[1]);
84
+
85
+ // Filter only Visual and Service components
86
+ this.componentsRegistry = this.filterOfficialComponents(allComponents);
87
+
88
+ Print.success('Component registry loaded successfully');
89
+
90
+ } catch (error) {
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Filters the registry to include ONLY Visual and Service category components
97
+ * Excludes AppComponents and any other category
98
+ * @param {object} allComponents - Object with all registry components
99
+ * @returns {object} - Filtered object with only Visual and Service
100
+ */
101
+ filterOfficialComponents(allComponents) {
102
+ const filtered = {};
103
+ let excludedCount = 0;
104
+
105
+ Object.entries(allComponents).forEach(([name, category]) => {
106
+ // Only include Visual or Service category components
107
+ if (category === 'Visual' || category === 'Service') {
108
+ filtered[name] = category;
109
+ } else {
110
+ excludedCount++;
111
+ }
112
+ });
113
+
114
+ if (excludedCount > 0) {
115
+ Print.info(`Filtered out ${excludedCount} non-Visual/Service components from registry`);
116
+ }
117
+
118
+ return filtered;
119
+ }
120
+
121
+ async getLocalComponents() {
122
+ try {
123
+ const componentsPath = getComponentsJsPath(import.meta.url);
124
+
125
+ if (!await fs.pathExists(componentsPath)) {
126
+ return {};
127
+ }
128
+
129
+ const content = await fs.readFile(componentsPath, 'utf8');
130
+ const match = content.match(/const components = ({[\s\S]*?});/);
131
+
132
+ if (!match) {
133
+ return {};
134
+ }
135
+
136
+ return JSON.parse(match[1]);
137
+ } catch (error) {
138
+ Print.warning(`⚠️ Could not read the local component registry at: ${componentsPath}`);
139
+ return {};
140
+ }
141
+ }
142
+
143
+ async findUpdatableComponents() {
144
+ await this._ensureConfig();
145
+ const localComponents = await this.getLocalComponents();
146
+ const updatableComponents = [];
147
+
148
+ Object.entries(localComponents).forEach(([name, category]) => {
149
+ // Check if component exists in remote registry
150
+ if (this.componentsRegistry[name] && this.componentsRegistry[name] === category) {
151
+ // Check if local component directory exists using dynamic paths
152
+ const categoryPath = validations.getCategoryPath(category);
153
+
154
+ // Use 4 levels for node_modules compatibility
155
+ const isProduction = this.config?.production?.enabled === true;
156
+ const folderSuffix = isProduction ? 'dist' : 'src';
157
+ const componentPath = getPath(import.meta.url, folderSuffix, categoryPath, name);
158
+
159
+ if (fs.pathExistsSync(componentPath)) {
160
+ updatableComponents.push({
161
+ name,
162
+ category,
163
+ path: componentPath
164
+ });
165
+ }
166
+ }
167
+ });
168
+
169
+ return updatableComponents;
170
+ }
171
+
172
+ getAvailableComponents(category = null) {
173
+ if (!this.componentsRegistry) return {};
174
+
175
+ const components = {};
176
+ Object.entries(this.componentsRegistry).forEach(([name, componentCategory]) => {
177
+ if (!category || componentCategory === category) {
178
+ // Special components that don't need all files
179
+ let files;
180
+ if (componentCategory === 'Visual') {
181
+ // Logical routing components only need JS
182
+ if (['Route', 'MultiRoute', 'Link'].includes(name)) {
183
+ files = [`${name}.js`];
184
+ } else {
185
+ // Normal visual components need JS, HTML, CSS
186
+ files = [`${name}.js`, `${name}.html`, `${name}.css`];
187
+ }
188
+ } else {
189
+ // Service components only need JS
190
+ files = [`${name}.js`];
191
+ }
192
+
193
+ components[name] = {
194
+ name,
195
+ category: componentCategory,
196
+ files: files
197
+ };
198
+ }
199
+ });
200
+
201
+ return components;
202
+ }
203
+
204
+
205
+ async downloadComponentFiles(componentName, category, targetPath) {
206
+ const component = this.getAvailableComponents(category)[componentName];
207
+
208
+ if (!component) {
209
+ throw new Error(`Component ${componentName} not found in ${category} category`);
210
+ }
211
+
212
+ const downloadedFiles = [];
213
+ const failedFiles = [];
214
+ const total = component.files.length;
215
+ let done = 0;
216
+ const spinner = ora(`Downloading ${componentName} 0/${total}`).start();
217
+ const worker = async (fileName) => {
218
+ const url = `${DOCS_REPO_BASE_URL}/${category}/${componentName}/${fileName}`;
219
+ const localPath = path.join(targetPath, fileName);
220
+ try {
221
+ const content = await fetchWithRetry(url);
222
+ await fs.writeFile(localPath, content, 'utf8');
223
+ downloadedFiles.push(fileName);
224
+ } catch (error) {
225
+ Print.downloadError(fileName);
226
+ failedFiles.push(fileName);
227
+ } finally {
228
+ done += 1;
229
+ spinner.text = `Downloading ${componentName} ${done}/${total}`;
230
+ }
231
+ };
232
+ await runConcurrent(component.files, worker, 3);
233
+ spinner.stop();
234
+
235
+ // Only throw error if main file (.js) was not downloaded
236
+ const mainFileDownloaded = downloadedFiles.some(file => file.endsWith('.js'));
237
+
238
+ if (!mainFileDownloaded) {
239
+ throw new Error(`Failed to download main component file (${componentName}.js)`);
240
+ }
241
+
242
+ // Report files that failed (but don't stop the process)
243
+ if (failedFiles.length > 0) {
244
+ Print.warning(`Some files couldn't be downloaded: ${failedFiles.join(', ')}`);
245
+ Print.info('Component installed with available files');
246
+ }
247
+
248
+ return downloadedFiles;
249
+ }
250
+
251
+ async updateLocalRegistrySafe(componentName, category) {
252
+ const componentsPath = getComponentsJsPath(import.meta.url);
253
+ try {
254
+ if (!await fs.pathExists(componentsPath)) {
255
+ const dir = path.dirname(componentsPath);
256
+ await fs.ensureDir(dir);
257
+ const initial = `const components = {};\n\nexport default components;\n`;
258
+ await fs.writeFile(componentsPath, initial, 'utf8');
259
+ }
260
+ const content = await fs.readFile(componentsPath, 'utf8');
261
+ const match = content.match(/const components = ({[\s\S]*?});/);
262
+ if (!match) throw new Error('Invalid components.js format in local project');
263
+ const componentsObj = JSON.parse(match[1]);
264
+ if (!componentsObj[componentName]) {
265
+ componentsObj[componentName] = category;
266
+ const sorted = Object.keys(componentsObj)
267
+ .sort()
268
+ .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
269
+ const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
270
+ await fs.writeFile(componentsPath, newContent, 'utf8');
271
+ Print.registryUpdate(`Registered ${componentName} in local components.js`);
272
+ } else {
273
+ Print.info(`${componentName} already exists in local registry`);
274
+ }
275
+ } catch (error) {
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ async installComponent(componentName, category, force = false) {
281
+ await this._ensureConfig();
282
+ const availableComponents = this.getAvailableComponents(category);
283
+
284
+ if (!availableComponents[componentName]) {
285
+ throw new Error(`Component '${componentName}' not found in category '${category}' in the official repository`);
286
+ }
287
+
288
+ // Detect if validations has access to configuration
289
+ let categoryPath;
290
+ const hasValidConfig = validations.config &&
291
+ validations.config.paths &&
292
+ validations.config.paths.components &&
293
+ validations.config.paths.components[category];
294
+
295
+ if (hasValidConfig) {
296
+ categoryPath = validations.getCategoryPath(category);
297
+ } else {
298
+ if (category === 'Visual') {
299
+ categoryPath = 'Components/Visual';
300
+ } else if (category === 'Service') {
301
+ categoryPath = 'Components/Service';
302
+ } else {
303
+ throw new Error(`Unknown category: ${category}`);
304
+ }
305
+ }
306
+
307
+ const isProduction = this.config?.production?.enabled === true;
308
+ const folderSuffix = isProduction ? 'dist' : 'src';
309
+
310
+ const cleanCategoryPath = categoryPath ? categoryPath.replace(/^[/\\]+/, '') : '';
311
+ const targetPath = getPath(import.meta.url, folderSuffix, cleanCategoryPath, componentName);
312
+
313
+
314
+
315
+ // Check if component already exists
316
+ if (await fs.pathExists(targetPath) && !force) {
317
+ const { overwrite } = await inquirer.prompt([
318
+ {
319
+ type: 'confirm',
320
+ name: 'overwrite',
321
+ message: `The component '${componentName}' already exists locally. Overwrite with the repository version?`,
322
+ default: false
323
+ }
324
+ ]);
325
+
326
+ if (!overwrite) {
327
+ Print.info('Installation cancelled by user');
328
+ return false;
329
+ }
330
+ }
331
+
332
+ try {
333
+ // Create component directory
334
+ await fs.ensureDir(targetPath);
335
+
336
+ // Download component files
337
+ const downloadedFiles = await this.downloadComponentFiles(componentName, category, targetPath);
338
+
339
+ await this.updateLocalRegistrySafe(componentName, category);
340
+
341
+ Print.success(`${componentName} installed successfully from official repository!`);
342
+ console.log(`📁 Location: ${folderSuffix}/${categoryPath}/${componentName}/`);
343
+ console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
344
+
345
+ return true;
346
+
347
+ } catch (error) {
348
+ // Only clean up if main file (.js) does not exist
349
+ const mainFilePath = path.join(targetPath, `${componentName}.js`);
350
+ const mainFileExists = await fs.pathExists(mainFilePath);
351
+
352
+ if (!mainFileExists && await fs.pathExists(targetPath)) {
353
+ // Only clean up if main file was not installed
354
+ await fs.remove(targetPath);
355
+ } else if (mainFileExists) {
356
+ Print.warning('Component partially installed - main file exists');
357
+ }
358
+
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ async installMultipleComponents(componentNames, category = 'Visual', force = false) {
364
+ const results = [];
365
+ Print.info(`Getting ${componentNames.length} ${category} components from official repository...`);
366
+ const total = componentNames.length;
367
+ let done = 0;
368
+ const spinner = ora(`Installing 0/${total}`).start();
369
+ const worker = async (componentName) => {
370
+ try {
371
+ const result = await this.installComponent(componentName, category, force);
372
+ results.push({ name: componentName, success: result });
373
+ } catch (error) {
374
+ Print.componentError(componentName, 'getting', error.message);
375
+ results.push({ name: componentName, success: false, error: error.message });
376
+ } finally {
377
+ done += 1;
378
+ spinner.text = `Installing ${done}/${total}`;
379
+ }
380
+ };
381
+ await runConcurrent(componentNames, worker, 3);
382
+ spinner.stop();
383
+
384
+ // Summary
385
+ const successful = results.filter(r => r.success).length;
386
+ const failed = results.filter(r => !r.success).length;
387
+
388
+ Print.newLine();
389
+ Print.summary(successful, failed, componentNames.length);
390
+
391
+ return results;
392
+ }
393
+
394
+ async updateAllComponents(force = false) {
395
+ Print.info('Looking for updatable Visual components...');
396
+
397
+ const allUpdatableComponents = await this.findUpdatableComponents();
398
+
399
+ // Filter only Visual components
400
+ const updatableComponents = allUpdatableComponents.filter(comp => comp.category === 'Visual');
401
+
402
+ if (updatableComponents.length === 0) {
403
+ Print.info('No local Visual components found that match the official repository');
404
+ Print.info('Use "slice browse" to see available components');
405
+ return true;
406
+ }
407
+
408
+ // Show statistics if there are Service components that won't be synced
409
+ const serviceComponents = allUpdatableComponents.filter(comp => comp.category === 'Service');
410
+ if (serviceComponents.length > 0) {
411
+ Print.info(`Found ${serviceComponents.length} Service components (skipped - sync only affects Visual components)`);
412
+ }
413
+
414
+ Print.newLine();
415
+ Print.subtitle(`Found ${updatableComponents.length} updatable Visual components:`);
416
+ Print.newLine();
417
+ updatableComponents.forEach(comp => {
418
+ console.log(`🎨 ${comp.name} (${comp.category})`);
419
+ });
420
+
421
+ if (!force) {
422
+ const { confirmUpdate } = await inquirer.prompt([
423
+ {
424
+ type: 'confirm',
425
+ name: 'confirmUpdate',
426
+ message: `Do you want to update these Visual components to the repository versions?`,
427
+ default: true
428
+ }
429
+ ]);
430
+
431
+ if (!confirmUpdate) {
432
+ Print.info('Update cancelled by user');
433
+ return false;
434
+ }
435
+ }
436
+
437
+ // Only update Visual components
438
+ const visualComponentNames = updatableComponents.map(c => c.name);
439
+
440
+ Print.info(`Updating ${visualComponentNames.length} Visual components...`);
441
+ const results = await this.installMultipleComponents(visualComponentNames, 'Visual', true);
442
+
443
+ // Final summary
444
+ const totalSuccessful = results.filter(r => r.success).length;
445
+ const totalFailed = results.filter(r => !r.success).length;
446
+
447
+ Print.newLine();
448
+ Print.title('Visual Components Sync Summary');
449
+ Print.success(`Visual components updated: ${totalSuccessful}`);
450
+
451
+ if (totalFailed > 0) {
452
+ Print.error(`Visual components failed: ${totalFailed}`);
453
+ } else {
454
+ Print.success('All your Visual components are now updated to the latest official versions!');
455
+ }
456
+
457
+ // Additional information about Service components
458
+ if (serviceComponents.length > 0) {
459
+ Print.newLine();
460
+ Print.info(`Note: ${serviceComponents.length} Service components were found but not updated`);
461
+ Print.info('Service components maintain manual versioning - update them individually if needed');
462
+ Print.commandExample('Update Service component manually', 'slice get FetchManager --service --force');
463
+ }
464
+
465
+ return totalFailed === 0;
466
+ }
467
+
468
+ displayAvailableComponents() {
469
+ if (!this.componentsRegistry) {
470
+ Print.error('❌ Could not load component registry');
471
+ return;
472
+ }
473
+
474
+ console.log('\n📚 Available components in the official Slice.js repository:\n');
475
+
476
+ const visualComponents = this.getAvailableComponents('Visual');
477
+ const serviceComponents = this.getAvailableComponents('Service');
478
+
479
+ // Only show names without descriptions
480
+ Print.info('🎨 Visual Components (UI):');
481
+ Object.keys(visualComponents).forEach(name => {
482
+ const files = visualComponents[name].files;
483
+ const fileIcons = files.map(file => {
484
+ if (file.endsWith('.js')) return '📜';
485
+ if (file.endsWith('.html')) return '🌐';
486
+ if (file.endsWith('.css')) return '🎨';
487
+ return '📄';
488
+ }).join(' ');
489
+ console.log(` ${name} ${fileIcons}`);
490
+ });
491
+
492
+ Print.info('\n⚙️ Service Components (Logic):');
493
+ Object.keys(serviceComponents).forEach(name => {
494
+ console.log(` • ${name} 📜`);
495
+ });
496
+
497
+ Print.newLine();
498
+ Print.info(`Total: ${Object.keys(visualComponents).length} Visual + ${Object.keys(serviceComponents).length} Service components`);
499
+
500
+ console.log(`\n💡 Usage examples:`);
501
+ console.log(`slice get Button Card Input # Install Visual components`);
502
+ console.log(`slice get FetchManager --service # Install Service component`);
503
+ console.log(`slice sync # Sync Visual components`);
504
+ }
505
+
506
+ async interactiveInstall() {
507
+ const { componentType } = await inquirer.prompt([
508
+ {
509
+ type: 'list',
510
+ name: 'componentType',
511
+ message: 'Select the type of component to install from the repository:',
512
+ choices: [
513
+ { name: '🎨 Visual Components (UI)', value: 'Visual' },
514
+ { name: '⚙️ Service Components (Logic)', value: 'Service' }
515
+ ]
516
+ }
517
+ ]);
518
+
519
+ const availableComponents = this.getAvailableComponents(componentType);
520
+ const componentChoices = Object.keys(availableComponents).map(name => ({
521
+ name: name,
522
+ value: name
523
+ }));
524
+
525
+ if (componentType === 'Visual') {
526
+ const { installMode } = await inquirer.prompt([
527
+ {
528
+ type: 'list',
529
+ name: 'installMode',
530
+ message: 'How do you want to install Visual components?',
531
+ choices: [
532
+ { name: 'Get one', value: 'single' },
533
+ { name: 'Get multiple', value: 'multiple' }
534
+ ]
535
+ }
536
+ ]);
537
+
538
+ if (installMode === 'multiple') {
539
+ const { selectedComponents } = await inquirer.prompt([
540
+ {
541
+ type: 'checkbox',
542
+ name: 'selectedComponents',
543
+ message: 'Select Visual components to install from the repository:',
544
+ choices: componentChoices,
545
+ validate: (input) => {
546
+ if (input.length === 0) {
547
+ return 'You must select at least one component';
548
+ }
549
+ return true;
550
+ }
551
+ }
552
+ ]);
553
+
554
+ await this.installMultipleComponents(selectedComponents, componentType);
555
+ } else {
556
+ const { selectedComponent } = await inquirer.prompt([
557
+ {
558
+ type: 'list',
559
+ name: 'selectedComponent',
560
+ message: 'Select a Visual component:',
561
+ choices: componentChoices
562
+ }
563
+ ]);
564
+
565
+ await this.installComponent(selectedComponent, componentType);
566
+ }
567
+ } else {
568
+ const { selectedComponent } = await inquirer.prompt([
569
+ {
570
+ type: 'list',
571
+ name: 'selectedComponent',
572
+ message: 'Select a Service component:',
573
+ choices: componentChoices
574
+ }
575
+ ]);
576
+
577
+ await this.installComponent(selectedComponent, componentType);
578
+ }
579
+ }
580
+
581
+ findComponentInRegistry(componentName) {
582
+ if (!this.componentsRegistry) return null;
583
+
584
+ const normalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
585
+
586
+ if (this.componentsRegistry[normalizedName]) {
587
+ return {
588
+ name: normalizedName,
589
+ category: this.componentsRegistry[normalizedName]
590
+ };
591
+ }
592
+
593
+ return null;
594
+ }
595
+ }
596
+
597
+ // Main get function
598
+ async function getComponents(componentNames = [], options = {}) {
599
+ const registry = new ComponentRegistry();
600
+
601
+ try {
602
+ await registry.loadRegistry();
603
+ } catch (error) {
604
+ Print.error('Could not load component registry from official repository');
605
+ Print.info('Check your internet connection and try again');
606
+ return false;
607
+ }
608
+
609
+ // Interactive mode if no components specified
610
+ if (!componentNames || componentNames.length === 0) {
611
+ await registry.interactiveInstall();
612
+ return true;
613
+ }
614
+
615
+ // Determine category
616
+ const category = options.service ? 'Service' : 'Visual';
617
+
618
+ if (componentNames.length === 1) {
619
+ // Single component install
620
+ const componentInfo = registry.findComponentInRegistry(componentNames[0]);
621
+
622
+ if (!componentInfo) {
623
+ Print.error(`Component '${componentNames[0]}' not found in official repository`);
624
+ Print.commandExample('View available components', 'slice browse');
625
+ return false;
626
+ }
627
+
628
+ // Use the category from registry unless Service is explicitly requested
629
+ const actualCategory = options.service ? 'Service' : componentInfo.category;
630
+
631
+ try {
632
+ await registry.installComponent(componentInfo.name, actualCategory, options.force);
633
+ return true;
634
+ } catch (error) {
635
+ Print.error(`Error installing component: ${error.message}`);
636
+ return false;
637
+ }
638
+ } else {
639
+ // Multiple components install
640
+ const normalizedComponents = componentNames.map(name =>
641
+ name.charAt(0).toUpperCase() + name.slice(1)
642
+ );
643
+
644
+ try {
645
+ await registry.installMultipleComponents(normalizedComponents, category, options.force);
646
+ return true;
647
+ } catch (error) {
648
+ Print.error(`Error installing components: ${error.message}`);
649
+ return false;
650
+ }
651
+ }
652
+ }
653
+
654
+ // List components function
655
+ async function listComponents() {
656
+ const registry = new ComponentRegistry();
657
+
658
+ try {
659
+ await registry.loadRegistry();
660
+ registry.displayAvailableComponents();
661
+ return true;
662
+ } catch (error) {
663
+ Print.error('Could not load component registry from official repository');
664
+ Print.info('Check your internet connection and try again');
665
+ return false;
666
+ }
667
+ }
668
+
669
+ // Sync components function
670
+ async function syncComponents(options = {}) {
671
+ const registry = new ComponentRegistry();
672
+
673
+ try {
674
+ await registry.loadRegistry();
675
+ return await registry.updateAllComponents(options.force);
676
+ } catch (error) {
677
+ Print.error('Could not load component registry from official repository');
678
+ Print.info('Check your internet connection and try again');
679
+ return false;
680
+ }
681
+ }
682
+
683
+ export default getComponents;
684
+ export { listComponents, syncComponents, ComponentRegistry, loadConfig, runConcurrent, fetchWithRetry };