slicejs-cli 3.6.3 → 3.6.5

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.
@@ -1,701 +1,695 @@
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, binary = false) => {
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 binary ? await response.arrayBuffer() : 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
- // Serializes read-modify-write cycles on components.js: installs run
54
- // concurrently (runConcurrent), and parallel writers corrupt the file
55
- // (partial reads) or silently drop registrations (lost updates).
56
- this._registryLock = Promise.resolve();
57
- }
58
-
59
- async _ensureConfig() {
60
- if (!this.config && !this._configPromise) {
61
- this._configPromise = loadConfig();
62
- }
63
- if (this._configPromise) {
64
- this.config = await this._configPromise;
65
- }
66
- }
67
-
68
- async loadRegistry() {
69
- await this._ensureConfig();
70
- Print.info('Loading component registry from official repository...');
71
-
72
- try {
73
- const response = await fetch(COMPONENTS_REGISTRY_URL);
74
-
75
- if (!response.ok) {
76
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
77
- }
78
-
79
- const content = await response.text();
80
-
81
- // Parse the components.js file content
82
- const match = content.match(/const components = ({[\s\S]*?});/);
83
- if (!match) {
84
- throw new Error('Invalid components.js format from repository');
85
- }
86
-
87
- const allComponents = JSON.parse(match[1]);
88
-
89
- // Filter only Visual and Service components
90
- this.componentsRegistry = this.filterOfficialComponents(allComponents);
91
-
92
- Print.success('Component registry loaded successfully');
93
-
94
- } catch (error) {
95
- throw error;
96
- }
97
- }
98
-
99
- /**
100
- * Filters the registry to include ONLY Visual and Service category components
101
- * Excludes AppComponents and any other category
102
- * @param {object} allComponents - Object with all registry components
103
- * @returns {object} - Filtered object with only Visual and Service
104
- */
105
- filterOfficialComponents(allComponents) {
106
- const filtered = {};
107
- let excludedCount = 0;
108
-
109
- Object.entries(allComponents).forEach(([name, category]) => {
110
- // Only include Visual or Service category components
111
- if (category === 'Visual' || category === 'Service') {
112
- filtered[name] = category;
113
- } else {
114
- excludedCount++;
115
- }
116
- });
117
-
118
- if (excludedCount > 0) {
119
- Print.info(`Filtered out ${excludedCount} non-Visual/Service components from registry`);
120
- }
121
-
122
- return filtered;
123
- }
124
-
125
- async getLocalComponents() {
126
- try {
127
- const componentsPath = getComponentsJsPath(import.meta.url);
128
-
129
- if (!await fs.pathExists(componentsPath)) {
130
- return {};
131
- }
132
-
133
- const content = await fs.readFile(componentsPath, 'utf8');
134
- const match = content.match(/const components = ({[\s\S]*?});/);
135
-
136
- if (!match) {
137
- return {};
138
- }
139
-
140
- return JSON.parse(match[1]);
141
- } catch (error) {
142
- Print.warning(`⚠️ Could not read the local component registry at: ${componentsPath}`);
143
- return {};
144
- }
145
- }
146
-
147
- async findUpdatableComponents() {
148
- await this._ensureConfig();
149
- const localComponents = await this.getLocalComponents();
150
- const updatableComponents = [];
151
-
152
- Object.entries(localComponents).forEach(([name, category]) => {
153
- // Check if component exists in remote registry
154
- if (this.componentsRegistry[name] && this.componentsRegistry[name] === category) {
155
- // Check if local component directory exists using dynamic paths
156
- const categoryPath = validations.getCategoryPath(category);
157
-
158
- // Use 4 levels for node_modules compatibility
159
- const isProduction = this.config?.production?.enabled === true;
160
- const folderSuffix = isProduction ? 'dist' : 'src';
161
- const componentPath = getPath(import.meta.url, folderSuffix, categoryPath, name);
162
-
163
- if (fs.pathExistsSync(componentPath)) {
164
- updatableComponents.push({
165
- name,
166
- category,
167
- path: componentPath
168
- });
169
- }
170
- }
171
- });
172
-
173
- return updatableComponents;
174
- }
175
-
176
- getAvailableComponents(category = null) {
177
- if (!this.componentsRegistry) return {};
178
-
179
- const components = {};
180
- Object.entries(this.componentsRegistry).forEach(([name, componentCategory]) => {
181
- if (!category || componentCategory === category) {
182
- // Special components that don't need all files
183
- let files;
184
- if (componentCategory === 'Visual') {
185
- // Logical routing components only need JS
186
- if (['Route', 'MultiRoute', 'Link'].includes(name)) {
187
- files = [`${name}.js`];
188
- } else if (name === 'Icon') {
189
- // Icon component needs font files to render glyphs
190
- files = [`${name}.js`, `${name}.html`, `${name}.css`, 'slc.eot', 'slc.woff2', 'slc.woff', 'slc.ttf', 'slc.svg'];
191
- } else {
192
- // Normal visual components need JS, HTML, CSS
193
- files = [`${name}.js`, `${name}.html`, `${name}.css`];
194
- }
195
- } else {
196
- // Service components only need JS
197
- files = [`${name}.js`];
198
- }
199
-
200
- components[name] = {
201
- name,
202
- category: componentCategory,
203
- files: files
204
- };
205
- }
206
- });
207
-
208
- return components;
209
- }
210
-
211
-
212
- async downloadComponentFiles(componentName, category, targetPath) {
213
- const component = this.getAvailableComponents(category)[componentName];
214
-
215
- if (!component) {
216
- throw new Error(`Component ${componentName} not found in ${category} category`);
217
- }
218
-
219
- const downloadedFiles = [];
220
- const failedFiles = [];
221
- const total = component.files.length;
222
- let done = 0;
223
- const spinner = ora(`Downloading ${componentName} 0/${total}`).start();
224
- const BINARY_EXTENSIONS = ['.eot', '.woff2', '.woff', '.ttf'];
225
- const worker = async (fileName) => {
226
- const url = `${DOCS_REPO_BASE_URL}/${category}/${componentName}/${fileName}`;
227
- const localPath = path.join(targetPath, fileName);
228
- const isBinary = BINARY_EXTENSIONS.some(ext => fileName.endsWith(ext));
229
- try {
230
- const content = await fetchWithRetry(url, 3, 500, isBinary);
231
- if (isBinary) {
232
- await fs.writeFile(localPath, Buffer.from(content));
233
- } else {
234
- await fs.writeFile(localPath, content, 'utf8');
235
- }
236
- downloadedFiles.push(fileName);
237
- } catch (error) {
238
- Print.downloadError(fileName);
239
- failedFiles.push(fileName);
240
- } finally {
241
- done += 1;
242
- spinner.text = `Downloading ${componentName} ${done}/${total}`;
243
- }
244
- };
245
- await runConcurrent(component.files, worker, 3);
246
- spinner.stop();
247
-
248
- // Only throw error if main file (.js) was not downloaded
249
- const mainFileDownloaded = downloadedFiles.some(file => file.endsWith('.js'));
250
-
251
- if (!mainFileDownloaded) {
252
- throw new Error(`Failed to download main component file (${componentName}.js)`);
253
- }
254
-
255
- // Report files that failed (but don't stop the process)
256
- if (failedFiles.length > 0) {
257
- Print.warning(`Some files couldn't be downloaded: ${failedFiles.join(', ')}`);
258
- Print.info('Component installed with available files');
259
- }
260
-
261
- return downloadedFiles;
262
- }
263
-
264
- async updateLocalRegistrySafe(componentName, category) {
265
- // Queue behind any in-flight registry update; keep the chain alive even
266
- // when an update throws so later updates still run.
267
- const run = this._registryLock.then(() => this._updateLocalRegistry(componentName, category));
268
- this._registryLock = run.catch(() => {});
269
- return run;
270
- }
271
-
272
- async _updateLocalRegistry(componentName, category) {
273
- const componentsPath = getComponentsJsPath(import.meta.url);
274
- if (!await fs.pathExists(componentsPath)) {
275
- const dir = path.dirname(componentsPath);
276
- await fs.ensureDir(dir);
277
- const initial = `const components = {};\n\nexport default components;\n`;
278
- await fs.writeFile(componentsPath, initial, 'utf8');
279
- }
280
- const content = await fs.readFile(componentsPath, 'utf8');
281
- const match = content.match(/const components = ({[\s\S]*?});/);
282
- if (!match) throw new Error('Invalid components.js format in local project');
283
- const componentsObj = JSON.parse(match[1]);
284
- if (!componentsObj[componentName]) {
285
- componentsObj[componentName] = category;
286
- const sorted = Object.keys(componentsObj)
287
- .sort()
288
- .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
289
- const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
290
- await fs.writeFile(componentsPath, newContent, 'utf8');
291
- Print.registryUpdate(`Registered ${componentName} in local components.js`);
292
- } else {
293
- Print.info(`${componentName} already exists in local registry`);
294
- }
295
- }
296
-
297
- async installComponent(componentName, category, force = false) {
298
- await this._ensureConfig();
299
- const availableComponents = this.getAvailableComponents(category);
300
-
301
- if (!availableComponents[componentName]) {
302
- throw new Error(`Component '${componentName}' not found in category '${category}' in the official repository`);
303
- }
304
-
305
- // Detect if validations has access to configuration
306
- let categoryPath;
307
- const hasValidConfig = validations.config &&
308
- validations.config.paths &&
309
- validations.config.paths.components &&
310
- validations.config.paths.components[category];
311
-
312
- if (hasValidConfig) {
313
- categoryPath = validations.getCategoryPath(category);
314
- } else {
315
- if (category === 'Visual') {
316
- categoryPath = 'Components/Visual';
317
- } else if (category === 'Service') {
318
- categoryPath = 'Components/Service';
319
- } else {
320
- throw new Error(`Unknown category: ${category}`);
321
- }
322
- }
323
-
324
- const isProduction = this.config?.production?.enabled === true;
325
- const folderSuffix = isProduction ? 'dist' : 'src';
326
-
327
- const cleanCategoryPath = categoryPath ? categoryPath.replace(/^[/\\]+/, '') : '';
328
- const targetPath = getPath(import.meta.url, folderSuffix, cleanCategoryPath, componentName);
329
-
330
-
331
-
332
- // Check if component already exists
333
- if (await fs.pathExists(targetPath) && !force) {
334
- const { overwrite } = await inquirer.prompt([
335
- {
336
- type: 'confirm',
337
- name: 'overwrite',
338
- message: `The component '${componentName}' already exists locally. Overwrite with the repository version?`,
339
- default: false
340
- }
341
- ]);
342
-
343
- if (!overwrite) {
344
- Print.info('Installation cancelled by user');
345
- return false;
346
- }
347
- }
348
-
349
- try {
350
- // Create component directory
351
- await fs.ensureDir(targetPath);
352
-
353
- // Download component files
354
- const downloadedFiles = await this.downloadComponentFiles(componentName, category, targetPath);
355
-
356
- await this.updateLocalRegistrySafe(componentName, category);
357
-
358
- Print.success(`${componentName} installed successfully from official repository!`);
359
- console.log(`📁 Location: ${[folderSuffix, categoryPath, componentName].join('/').replace(/\/+/g, '/')}/`);
360
- console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
361
-
362
- return true;
363
-
364
- } catch (error) {
365
- // Only clean up if main file (.js) does not exist
366
- const mainFilePath = path.join(targetPath, `${componentName}.js`);
367
- const mainFileExists = await fs.pathExists(mainFilePath);
368
-
369
- if (!mainFileExists && await fs.pathExists(targetPath)) {
370
- // Only clean up if main file was not installed
371
- await fs.remove(targetPath);
372
- } else if (mainFileExists) {
373
- Print.warning('Component partially installed - main file exists');
374
- }
375
-
376
- throw error;
377
- }
378
- }
379
-
380
- async installMultipleComponents(componentNames, category = 'Visual', force = false) {
381
- const results = [];
382
- Print.info(`Getting ${componentNames.length} ${category} components from official repository...`);
383
- const total = componentNames.length;
384
- let done = 0;
385
- const spinner = ora(`Installing 0/${total}`).start();
386
- const worker = async (componentName) => {
387
- try {
388
- const result = await this.installComponent(componentName, category, force);
389
- results.push({ name: componentName, success: result });
390
- } catch (error) {
391
- Print.componentError(componentName, 'getting', error.message);
392
- results.push({ name: componentName, success: false, error: error.message });
393
- } finally {
394
- done += 1;
395
- spinner.text = `Installing ${done}/${total}`;
396
- }
397
- };
398
- await runConcurrent(componentNames, worker, 3);
399
- spinner.stop();
400
-
401
- // Summary
402
- const successful = results.filter(r => r.success).length;
403
- const failed = results.filter(r => !r.success).length;
404
-
405
- Print.newLine();
406
- Print.summary(successful, failed, componentNames.length);
407
-
408
- return results;
409
- }
410
-
411
- async updateAllComponents(force = false) {
412
- Print.info('Looking for updatable Visual components...');
413
-
414
- const allUpdatableComponents = await this.findUpdatableComponents();
415
-
416
- // Filter only Visual components
417
- const updatableComponents = allUpdatableComponents.filter(comp => comp.category === 'Visual');
418
-
419
- if (updatableComponents.length === 0) {
420
- Print.info('No local Visual components found that match the official repository');
421
- Print.info('Use "slice browse" to see available components');
422
- return true;
423
- }
424
-
425
- // Show statistics if there are Service components that won't be synced
426
- const serviceComponents = allUpdatableComponents.filter(comp => comp.category === 'Service');
427
- if (serviceComponents.length > 0) {
428
- Print.info(`Found ${serviceComponents.length} Service components (skipped - sync only affects Visual components)`);
429
- }
430
-
431
- Print.newLine();
432
- Print.subtitle(`Found ${updatableComponents.length} updatable Visual components:`);
433
- Print.newLine();
434
- updatableComponents.forEach(comp => {
435
- console.log(`🎨 ${comp.name} (${comp.category})`);
436
- });
437
-
438
- if (!force) {
439
- const { confirmUpdate } = await inquirer.prompt([
440
- {
441
- type: 'confirm',
442
- name: 'confirmUpdate',
443
- message: `Do you want to update these Visual components to the repository versions?`,
444
- default: true
445
- }
446
- ]);
447
-
448
- if (!confirmUpdate) {
449
- Print.info('Update cancelled by user');
450
- return false;
451
- }
452
- }
453
-
454
- // Only update Visual components
455
- const visualComponentNames = updatableComponents.map(c => c.name);
456
-
457
- Print.info(`Updating ${visualComponentNames.length} Visual components...`);
458
- const results = await this.installMultipleComponents(visualComponentNames, 'Visual', true);
459
-
460
- // Final summary
461
- const totalSuccessful = results.filter(r => r.success).length;
462
- const totalFailed = results.filter(r => !r.success).length;
463
-
464
- Print.newLine();
465
- Print.title('Visual Components Sync Summary');
466
- Print.success(`Visual components updated: ${totalSuccessful}`);
467
-
468
- if (totalFailed > 0) {
469
- Print.error(`Visual components failed: ${totalFailed}`);
470
- } else {
471
- Print.success('All your Visual components are now updated to the latest official versions!');
472
- }
473
-
474
- // Additional information about Service components
475
- if (serviceComponents.length > 0) {
476
- Print.newLine();
477
- Print.info(`Note: ${serviceComponents.length} Service components were found but not updated`);
478
- Print.info('Service components maintain manual versioning - update them individually if needed');
479
- Print.commandExample('Update Service component manually', 'slice get FetchManager --service --force');
480
- }
481
-
482
- return totalFailed === 0;
483
- }
484
-
485
- displayAvailableComponents() {
486
- if (!this.componentsRegistry) {
487
- Print.error('❌ Could not load component registry');
488
- return;
489
- }
490
-
491
- console.log('\n📚 Available components in the official Slice.js repository:\n');
492
-
493
- const visualComponents = this.getAvailableComponents('Visual');
494
- const serviceComponents = this.getAvailableComponents('Service');
495
-
496
- // Only show names without descriptions
497
- Print.info('🎨 Visual Components (UI):');
498
- Object.keys(visualComponents).forEach(name => {
499
- const files = visualComponents[name].files;
500
- const fileIcons = files.map(file => {
501
- if (file.endsWith('.js')) return '📜';
502
- if (file.endsWith('.html')) return '🌐';
503
- if (file.endsWith('.css')) return '🎨';
504
- return '📄';
505
- }).join(' ');
506
- console.log(` • ${name} ${fileIcons}`);
507
- });
508
-
509
- Print.info('\n⚙️ Service Components (Logic):');
510
- Object.keys(serviceComponents).forEach(name => {
511
- console.log(` • ${name} 📜`);
512
- });
513
-
514
- Print.newLine();
515
- Print.info(`Total: ${Object.keys(visualComponents).length} Visual + ${Object.keys(serviceComponents).length} Service components`);
516
-
517
- console.log(`\n💡 Usage examples:`);
518
- console.log(`slice get Button Card Input # Install Visual components`);
519
- console.log(`slice get FetchManager --service # Install Service component`);
520
- console.log(`slice sync # Sync Visual components`);
521
- }
522
-
523
- async interactiveInstall() {
524
- const { componentType } = await inquirer.prompt([
525
- {
526
- type: 'list',
527
- name: 'componentType',
528
- message: 'Select the type of component to install from the repository:',
529
- choices: [
530
- { name: '🎨 Visual Components (UI)', value: 'Visual' },
531
- { name: '⚙️ Service Components (Logic)', value: 'Service' }
532
- ]
533
- }
534
- ]);
535
-
536
- const availableComponents = this.getAvailableComponents(componentType);
537
- const componentChoices = Object.keys(availableComponents).map(name => ({
538
- name: name,
539
- value: name
540
- }));
541
-
542
- if (componentType === 'Visual') {
543
- const { installMode } = await inquirer.prompt([
544
- {
545
- type: 'list',
546
- name: 'installMode',
547
- message: 'How do you want to install Visual components?',
548
- choices: [
549
- { name: 'Get one', value: 'single' },
550
- { name: 'Get multiple', value: 'multiple' }
551
- ]
552
- }
553
- ]);
554
-
555
- if (installMode === 'multiple') {
556
- const { selectedComponents } = await inquirer.prompt([
557
- {
558
- type: 'checkbox',
559
- name: 'selectedComponents',
560
- message: 'Select Visual components to install from the repository:',
561
- choices: componentChoices,
562
- validate: (input) => {
563
- if (input.length === 0) {
564
- return 'You must select at least one component';
565
- }
566
- return true;
567
- }
568
- }
569
- ]);
570
-
571
- await this.installMultipleComponents(selectedComponents, componentType);
572
- } else {
573
- const { selectedComponent } = await inquirer.prompt([
574
- {
575
- type: 'list',
576
- name: 'selectedComponent',
577
- message: 'Select a Visual component:',
578
- choices: componentChoices
579
- }
580
- ]);
581
-
582
- await this.installComponent(selectedComponent, componentType);
583
- }
584
- } else {
585
- const { selectedComponent } = await inquirer.prompt([
586
- {
587
- type: 'list',
588
- name: 'selectedComponent',
589
- message: 'Select a Service component:',
590
- choices: componentChoices
591
- }
592
- ]);
593
-
594
- await this.installComponent(selectedComponent, componentType);
595
- }
596
- }
597
-
598
- findComponentInRegistry(componentName) {
599
- if (!this.componentsRegistry) return null;
600
-
601
- const normalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
602
-
603
- if (this.componentsRegistry[normalizedName]) {
604
- return {
605
- name: normalizedName,
606
- category: this.componentsRegistry[normalizedName]
607
- };
608
- }
609
-
610
- return null;
611
- }
612
- }
613
-
614
- // Main get function
615
- async function getComponents(componentNames = [], options = {}) {
616
- const registry = new ComponentRegistry();
617
-
618
- try {
619
- await registry.loadRegistry();
620
- } catch (error) {
621
- Print.error('Could not load component registry from official repository');
622
- Print.info('Check your internet connection and try again');
623
- return false;
624
- }
625
-
626
- // Interactive mode if no components specified
627
- if (!componentNames || componentNames.length === 0) {
628
- await registry.interactiveInstall();
629
- return true;
630
- }
631
-
632
- // Determine category
633
- const category = options.service ? 'Service' : 'Visual';
634
-
635
- if (componentNames.length === 1) {
636
- // Single component install
637
- const componentInfo = registry.findComponentInRegistry(componentNames[0]);
638
-
639
- if (!componentInfo) {
640
- Print.error(`Component '${componentNames[0]}' not found in official repository`);
641
- Print.commandExample('View available components', 'slice browse');
642
- return false;
643
- }
644
-
645
- // Use the category from registry unless Service is explicitly requested
646
- const actualCategory = options.service ? 'Service' : componentInfo.category;
647
-
648
- try {
649
- await registry.installComponent(componentInfo.name, actualCategory, options.force);
650
- return true;
651
- } catch (error) {
652
- Print.error(`Error installing component: ${error.message}`);
653
- return false;
654
- }
655
- } else {
656
- // Multiple components install
657
- const normalizedComponents = componentNames.map(name =>
658
- name.charAt(0).toUpperCase() + name.slice(1)
659
- );
660
-
661
- try {
662
- await registry.installMultipleComponents(normalizedComponents, category, options.force);
663
- return true;
664
- } catch (error) {
665
- Print.error(`Error installing components: ${error.message}`);
666
- return false;
667
- }
668
- }
669
- }
670
-
671
- // List components function
672
- async function listComponents() {
673
- const registry = new ComponentRegistry();
674
-
675
- try {
676
- await registry.loadRegistry();
677
- registry.displayAvailableComponents();
678
- return true;
679
- } catch (error) {
680
- Print.error('Could not load component registry from official repository');
681
- Print.info('Check your internet connection and try again');
682
- return false;
683
- }
684
- }
685
-
686
- // Sync components function
687
- async function syncComponents(options = {}) {
688
- const registry = new ComponentRegistry();
689
-
690
- try {
691
- await registry.loadRegistry();
692
- return await registry.updateAllComponents(options.force);
693
- } catch (error) {
694
- Print.error('Could not load component registry from official repository');
695
- Print.info('Check your internet connection and try again');
696
- return false;
697
- }
698
- }
699
-
700
- export default getComponents;
701
- export { listComponents, syncComponents, ComponentRegistry, loadConfig, runConcurrent, fetchWithRetry };
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, binary = false) => {
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 binary ? await response.arrayBuffer() : 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
+ // Serializes read-modify-write cycles on components.js: installs run
54
+ // concurrently (runConcurrent), and parallel writers corrupt the file
55
+ // (partial reads) or silently drop registrations (lost updates).
56
+ this._registryLock = Promise.resolve();
57
+ }
58
+
59
+ async _ensureConfig() {
60
+ if (!this.config && !this._configPromise) {
61
+ this._configPromise = loadConfig();
62
+ }
63
+ if (this._configPromise) {
64
+ this.config = await this._configPromise;
65
+ }
66
+ }
67
+
68
+ async loadRegistry() {
69
+ await this._ensureConfig();
70
+ Print.info('Loading component registry from official repository...');
71
+
72
+ try {
73
+ const response = await fetch(COMPONENTS_REGISTRY_URL);
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
77
+ }
78
+
79
+ const content = await response.text();
80
+
81
+ // Parse the components.js file content
82
+ const match = content.match(/const components = ({[\s\S]*?});/);
83
+ if (!match) {
84
+ throw new Error('Invalid components.js format from repository');
85
+ }
86
+
87
+ const allComponents = JSON.parse(match[1]);
88
+
89
+ // Filter only Visual and Service components
90
+ this.componentsRegistry = this.filterOfficialComponents(allComponents);
91
+
92
+ Print.success('Component registry loaded successfully');
93
+
94
+ } catch (error) {
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Filters the registry to include ONLY Visual and Service category components
101
+ * Excludes AppComponents and any other category
102
+ * @param {object} allComponents - Object with all registry components
103
+ * @returns {object} - Filtered object with only Visual and Service
104
+ */
105
+ filterOfficialComponents(allComponents) {
106
+ const filtered = {};
107
+ let excludedCount = 0;
108
+
109
+ Object.entries(allComponents).forEach(([name, category]) => {
110
+ // Only include Visual or Service category components
111
+ if (category === 'Visual' || category === 'Service') {
112
+ filtered[name] = category;
113
+ } else {
114
+ excludedCount++;
115
+ }
116
+ });
117
+
118
+ if (excludedCount > 0) {
119
+ Print.info(`Filtered out ${excludedCount} non-Visual/Service components from registry`);
120
+ }
121
+
122
+ return filtered;
123
+ }
124
+
125
+ async getLocalComponents() {
126
+ try {
127
+ const componentsPath = getComponentsJsPath(import.meta.url);
128
+
129
+ if (!await fs.pathExists(componentsPath)) {
130
+ return {};
131
+ }
132
+
133
+ const content = await fs.readFile(componentsPath, 'utf8');
134
+ const match = content.match(/const components = ({[\s\S]*?});/);
135
+
136
+ if (!match) {
137
+ return {};
138
+ }
139
+
140
+ return JSON.parse(match[1]);
141
+ } catch (error) {
142
+ Print.warning(`⚠️ Could not read the local component registry at: ${componentsPath}`);
143
+ return {};
144
+ }
145
+ }
146
+
147
+ async findUpdatableComponents() {
148
+ await this._ensureConfig();
149
+ const localComponents = await this.getLocalComponents();
150
+ const updatableComponents = [];
151
+
152
+ Object.entries(localComponents).forEach(([name, category]) => {
153
+ // Check if component exists in remote registry
154
+ if (this.componentsRegistry[name] && this.componentsRegistry[name] === category) {
155
+ // Check if local component directory exists using dynamic paths
156
+ const categoryPath = validations.getCategoryPath(category);
157
+
158
+ // Use 4 levels for node_modules compatibility
159
+ const isProduction = this.config?.production?.enabled === true;
160
+ const folderSuffix = isProduction ? 'dist' : 'src';
161
+ const componentPath = getPath(import.meta.url, folderSuffix, categoryPath, name);
162
+
163
+ if (fs.pathExistsSync(componentPath)) {
164
+ updatableComponents.push({
165
+ name,
166
+ category,
167
+ path: componentPath
168
+ });
169
+ }
170
+ }
171
+ });
172
+
173
+ return updatableComponents;
174
+ }
175
+
176
+ getAvailableComponents(category = null) {
177
+ if (!this.componentsRegistry) return {};
178
+
179
+ const components = {};
180
+ Object.entries(this.componentsRegistry).forEach(([name, componentCategory]) => {
181
+ if (!category || componentCategory === category) {
182
+ // Special components that don't need all files
183
+ let files;
184
+ if (componentCategory === 'Visual') {
185
+ // Logical routing components only need JS
186
+ if (['Route', 'MultiRoute', 'Link'].includes(name)) {
187
+ files = [`${name}.js`];
188
+ } else if (name === 'Icon') {
189
+ // Icon component needs font files to render glyphs
190
+ files = [`${name}.js`, `${name}.html`, `${name}.css`, 'slc.eot', 'slc.woff2', 'slc.woff', 'slc.ttf', 'slc.svg'];
191
+ } else {
192
+ // Normal visual components need JS, HTML, CSS
193
+ files = [`${name}.js`, `${name}.html`, `${name}.css`];
194
+ }
195
+ } else {
196
+ // Service components only need JS
197
+ files = [`${name}.js`];
198
+ }
199
+
200
+ components[name] = {
201
+ name,
202
+ category: componentCategory,
203
+ files: files
204
+ };
205
+ }
206
+ });
207
+
208
+ return components;
209
+ }
210
+
211
+
212
+ async downloadComponentFiles(componentName, category, targetPath) {
213
+ const component = this.getAvailableComponents(category)[componentName];
214
+
215
+ if (!component) {
216
+ throw new Error(`Component ${componentName} not found in ${category} category`);
217
+ }
218
+
219
+ const downloadedFiles = [];
220
+ const failedFiles = [];
221
+ const total = component.files.length;
222
+ let done = 0;
223
+ const spinner = ora(`Downloading ${componentName} 0/${total}`).start();
224
+ const BINARY_EXTENSIONS = ['.eot', '.woff2', '.woff', '.ttf'];
225
+ const worker = async (fileName) => {
226
+ const url = `${DOCS_REPO_BASE_URL}/${category}/${componentName}/${fileName}`;
227
+ const localPath = path.join(targetPath, fileName);
228
+ const isBinary = BINARY_EXTENSIONS.some(ext => fileName.endsWith(ext));
229
+ try {
230
+ const content = await fetchWithRetry(url, 3, 500, isBinary);
231
+ if (isBinary) {
232
+ await fs.writeFile(localPath, Buffer.from(content));
233
+ } else {
234
+ await fs.writeFile(localPath, content, 'utf8');
235
+ }
236
+ downloadedFiles.push(fileName);
237
+ } catch (error) {
238
+ Print.downloadError(fileName);
239
+ failedFiles.push(fileName);
240
+ } finally {
241
+ done += 1;
242
+ spinner.text = `Downloading ${componentName} ${done}/${total}`;
243
+ }
244
+ };
245
+ await runConcurrent(component.files, worker, 3);
246
+ spinner.stop();
247
+
248
+ // Only throw error if main file (.js) was not downloaded
249
+ const mainFileDownloaded = downloadedFiles.some(file => file.endsWith('.js'));
250
+
251
+ if (!mainFileDownloaded) {
252
+ throw new Error(`Failed to download main component file (${componentName}.js)`);
253
+ }
254
+
255
+ // Report files that failed (but don't stop the process)
256
+ if (failedFiles.length > 0) {
257
+ Print.warning(`Some files couldn't be downloaded: ${failedFiles.join(', ')}`);
258
+ Print.info('Component installed with available files');
259
+ }
260
+
261
+ return downloadedFiles;
262
+ }
263
+
264
+ async updateLocalRegistrySafe(componentName, category) {
265
+ const run = this._registryLock.then(() => this._updateLocalRegistry(componentName, category));
266
+ this._registryLock = run.catch(() => { /* non-critical: registry update failure is handled by caller */ });
267
+ return run;
268
+ }
269
+
270
+ async _updateLocalRegistry(componentName, category) {
271
+ const componentsPath = getComponentsJsPath(import.meta.url);
272
+ if (!await fs.pathExists(componentsPath)) {
273
+ const dir = path.dirname(componentsPath);
274
+ await fs.ensureDir(dir);
275
+ const initial = `const components = {};\n\nexport default components;\n`;
276
+ await fs.writeFile(componentsPath, initial, 'utf8');
277
+ }
278
+ const content = await fs.readFile(componentsPath, 'utf8');
279
+ const match = content.match(/const components = ({[\s\S]*?});/);
280
+ if (!match) throw new Error('Invalid components.js format in local project');
281
+ const componentsObj = JSON.parse(match[1]);
282
+ if (!componentsObj[componentName]) {
283
+ componentsObj[componentName] = category;
284
+ const sorted = Object.keys(componentsObj)
285
+ .sort()
286
+ .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
287
+ const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
288
+ await fs.writeFile(componentsPath, newContent, 'utf8');
289
+ Print.registryUpdate(`Registered ${componentName} in local components.js`);
290
+ } else {
291
+ Print.info(`${componentName} already exists in local registry`);
292
+ }
293
+ }
294
+
295
+ async installComponent(componentName, category, force = false) {
296
+ await this._ensureConfig();
297
+ const availableComponents = this.getAvailableComponents(category);
298
+
299
+ if (!availableComponents[componentName]) {
300
+ throw new Error(`Component '${componentName}' not found in category '${category}' in the official repository`);
301
+ }
302
+
303
+ // Detect if validations has access to configuration
304
+ let categoryPath;
305
+ const hasValidConfig = validations.config &&
306
+ validations.config.paths &&
307
+ validations.config.paths.components &&
308
+ validations.config.paths.components[category];
309
+
310
+ if (hasValidConfig) {
311
+ categoryPath = validations.getCategoryPath(category);
312
+ } else {
313
+ if (category === 'Visual') {
314
+ categoryPath = 'Components/Visual';
315
+ } else if (category === 'Service') {
316
+ categoryPath = 'Components/Service';
317
+ } else {
318
+ throw new Error(`Unknown category: ${category}`);
319
+ }
320
+ }
321
+
322
+ const isProduction = this.config?.production?.enabled === true;
323
+ const folderSuffix = isProduction ? 'dist' : 'src';
324
+
325
+ const cleanCategoryPath = categoryPath ? categoryPath.replace(/^[/\\]+/, '') : '';
326
+ const targetPath = getPath(import.meta.url, folderSuffix, cleanCategoryPath, componentName);
327
+
328
+
329
+
330
+ // Check if component already exists
331
+ if (await fs.pathExists(targetPath) && !force) {
332
+ const { overwrite } = await inquirer.prompt([
333
+ {
334
+ type: 'confirm',
335
+ name: 'overwrite',
336
+ message: `The component '${componentName}' already exists locally. Overwrite with the repository version?`,
337
+ default: false
338
+ }
339
+ ]);
340
+
341
+ if (!overwrite) {
342
+ Print.info('Installation cancelled by user');
343
+ return false;
344
+ }
345
+ }
346
+
347
+ try {
348
+ // Create component directory
349
+ await fs.ensureDir(targetPath);
350
+
351
+ // Download component files
352
+ const downloadedFiles = await this.downloadComponentFiles(componentName, category, targetPath);
353
+
354
+ await this.updateLocalRegistrySafe(componentName, category);
355
+
356
+ Print.success(`${componentName} installed successfully from official repository!`);
357
+ console.log(`📁 Location: ${[folderSuffix, categoryPath, componentName].join('/').replace(/\/+/g, '/')}/`);
358
+ console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
359
+
360
+ return true;
361
+
362
+ } catch (error) {
363
+ // Only clean up if main file (.js) does not exist
364
+ const mainFilePath = path.join(targetPath, `${componentName}.js`);
365
+ const mainFileExists = await fs.pathExists(mainFilePath);
366
+
367
+ if (!mainFileExists && await fs.pathExists(targetPath)) {
368
+ // Only clean up if main file was not installed
369
+ await fs.remove(targetPath);
370
+ } else if (mainFileExists) {
371
+ Print.warning('Component partially installed - main file exists');
372
+ }
373
+
374
+ throw error;
375
+ }
376
+ }
377
+
378
+ async installMultipleComponents(componentNames, category = 'Visual', force = false) {
379
+ const results = [];
380
+ Print.info(`Getting ${componentNames.length} ${category} components from official repository...`);
381
+ const total = componentNames.length;
382
+ let done = 0;
383
+ const spinner = ora(`Installing 0/${total}`).start();
384
+ const worker = async (componentName) => {
385
+ try {
386
+ const result = await this.installComponent(componentName, category, force);
387
+ results.push({ name: componentName, success: result });
388
+ } catch (error) {
389
+ Print.componentError(componentName, 'getting', error.message);
390
+ results.push({ name: componentName, success: false, error: error.message });
391
+ } finally {
392
+ done += 1;
393
+ spinner.text = `Installing ${done}/${total}`;
394
+ }
395
+ };
396
+ await runConcurrent(componentNames, worker, 3);
397
+ spinner.stop();
398
+
399
+ // Summary
400
+ const successful = results.filter(r => r.success).length;
401
+ const failed = results.filter(r => !r.success).length;
402
+
403
+ Print.newLine();
404
+ Print.summary(successful, failed, componentNames.length);
405
+
406
+ return results;
407
+ }
408
+
409
+ async updateAllComponents(force = false) {
410
+ Print.info('Looking for updatable Visual components...');
411
+
412
+ const allUpdatableComponents = await this.findUpdatableComponents();
413
+
414
+ // Filter only Visual components
415
+ const updatableComponents = allUpdatableComponents.filter(comp => comp.category === 'Visual');
416
+
417
+ if (updatableComponents.length === 0) {
418
+ Print.info('No local Visual components found that match the official repository');
419
+ Print.info('Use "slice browse" to see available components');
420
+ return true;
421
+ }
422
+
423
+ // Show statistics if there are Service components that won't be synced
424
+ const serviceComponents = allUpdatableComponents.filter(comp => comp.category === 'Service');
425
+ if (serviceComponents.length > 0) {
426
+ Print.info(`Found ${serviceComponents.length} Service components (skipped - sync only affects Visual components)`);
427
+ }
428
+
429
+ Print.newLine();
430
+ Print.subtitle(`Found ${updatableComponents.length} updatable Visual components:`);
431
+ Print.newLine();
432
+ updatableComponents.forEach(comp => {
433
+ console.log(`🎨 ${comp.name} (${comp.category})`);
434
+ });
435
+
436
+ if (!force) {
437
+ const { confirmUpdate } = await inquirer.prompt([
438
+ {
439
+ type: 'confirm',
440
+ name: 'confirmUpdate',
441
+ message: `Do you want to update these Visual components to the repository versions?`,
442
+ default: true
443
+ }
444
+ ]);
445
+
446
+ if (!confirmUpdate) {
447
+ Print.info('Update cancelled by user');
448
+ return false;
449
+ }
450
+ }
451
+
452
+ // Only update Visual components
453
+ const visualComponentNames = updatableComponents.map(c => c.name);
454
+
455
+ Print.info(`Updating ${visualComponentNames.length} Visual components...`);
456
+ const results = await this.installMultipleComponents(visualComponentNames, 'Visual', true);
457
+
458
+ // Final summary
459
+ const totalSuccessful = results.filter(r => r.success).length;
460
+ const totalFailed = results.filter(r => !r.success).length;
461
+
462
+ Print.newLine();
463
+ Print.title('Visual Components Sync Summary');
464
+ Print.success(`Visual components updated: ${totalSuccessful}`);
465
+
466
+ if (totalFailed > 0) {
467
+ Print.error(`Visual components failed: ${totalFailed}`);
468
+ } else {
469
+ Print.success('All your Visual components are now updated to the latest official versions!');
470
+ }
471
+
472
+ // Additional information about Service components
473
+ if (serviceComponents.length > 0) {
474
+ Print.newLine();
475
+ Print.info(`Note: ${serviceComponents.length} Service components were found but not updated`);
476
+ Print.info('Service components maintain manual versioning - update them individually if needed');
477
+ Print.commandExample('Update Service component manually', 'slice get FetchManager --service --force');
478
+ }
479
+
480
+ return totalFailed === 0;
481
+ }
482
+
483
+ displayAvailableComponents() {
484
+ if (!this.componentsRegistry) {
485
+ Print.error('❌ Could not load component registry');
486
+ return;
487
+ }
488
+
489
+ console.log('\n📚 Available components in the official Slice.js repository:\n');
490
+
491
+ const visualComponents = this.getAvailableComponents('Visual');
492
+ const serviceComponents = this.getAvailableComponents('Service');
493
+
494
+ // Only show names without descriptions
495
+ Print.info('🎨 Visual Components (UI):');
496
+ Object.keys(visualComponents).forEach(name => {
497
+ const files = visualComponents[name].files;
498
+ const fileIcons = files.map(file => {
499
+ if (file.endsWith('.js')) return '📜';
500
+ if (file.endsWith('.html')) return '🌐';
501
+ if (file.endsWith('.css')) return '🎨';
502
+ return '📄';
503
+ }).join(' ');
504
+ console.log(` • ${name} ${fileIcons}`);
505
+ });
506
+
507
+ Print.info('\n⚙️ Service Components (Logic):');
508
+ Object.keys(serviceComponents).forEach(name => {
509
+ console.log(` ${name} 📜`);
510
+ });
511
+
512
+ Print.newLine();
513
+ Print.info(`Total: ${Object.keys(visualComponents).length} Visual + ${Object.keys(serviceComponents).length} Service components`);
514
+
515
+ console.log(`\n💡 Usage examples:`);
516
+ console.log(`slice get Button Card Input # Install Visual components`);
517
+ console.log(`slice get FetchManager --service # Install Service component`);
518
+ console.log(`slice sync # Sync Visual components`);
519
+ }
520
+
521
+ async interactiveInstall() {
522
+ const { componentType } = await inquirer.prompt([
523
+ {
524
+ type: 'list',
525
+ name: 'componentType',
526
+ message: 'Select the type of component to install from the repository:',
527
+ choices: [
528
+ { name: '🎨 Visual Components (UI)', value: 'Visual' },
529
+ { name: '⚙️ Service Components (Logic)', value: 'Service' }
530
+ ]
531
+ }
532
+ ]);
533
+
534
+ const availableComponents = this.getAvailableComponents(componentType);
535
+ const componentChoices = Object.keys(availableComponents).map(name => ({
536
+ name: name,
537
+ value: name
538
+ }));
539
+
540
+ if (componentType === 'Visual') {
541
+ const { installMode } = await inquirer.prompt([
542
+ {
543
+ type: 'list',
544
+ name: 'installMode',
545
+ message: 'How do you want to install Visual components?',
546
+ choices: [
547
+ { name: 'Get one', value: 'single' },
548
+ { name: 'Get multiple', value: 'multiple' }
549
+ ]
550
+ }
551
+ ]);
552
+
553
+ if (installMode === 'multiple') {
554
+ const { selectedComponents } = await inquirer.prompt([
555
+ {
556
+ type: 'checkbox',
557
+ name: 'selectedComponents',
558
+ message: 'Select Visual components to install from the repository:',
559
+ choices: componentChoices,
560
+ validate: (input) => {
561
+ if (input.length === 0) {
562
+ return 'You must select at least one component';
563
+ }
564
+ return true;
565
+ }
566
+ }
567
+ ]);
568
+
569
+ await this.installMultipleComponents(selectedComponents, componentType);
570
+ } else {
571
+ const { selectedComponent } = await inquirer.prompt([
572
+ {
573
+ type: 'list',
574
+ name: 'selectedComponent',
575
+ message: 'Select a Visual component:',
576
+ choices: componentChoices
577
+ }
578
+ ]);
579
+
580
+ await this.installComponent(selectedComponent, componentType);
581
+ }
582
+ } else {
583
+ const { selectedComponent } = await inquirer.prompt([
584
+ {
585
+ type: 'list',
586
+ name: 'selectedComponent',
587
+ message: 'Select a Service component:',
588
+ choices: componentChoices
589
+ }
590
+ ]);
591
+
592
+ await this.installComponent(selectedComponent, componentType);
593
+ }
594
+ }
595
+
596
+ findComponentInRegistry(componentName) {
597
+ if (!this.componentsRegistry) return null;
598
+
599
+ const normalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
600
+
601
+ if (this.componentsRegistry[normalizedName]) {
602
+ return {
603
+ name: normalizedName,
604
+ category: this.componentsRegistry[normalizedName]
605
+ };
606
+ }
607
+
608
+ return null;
609
+ }
610
+ }
611
+
612
+ const sharedRegistry = new ComponentRegistry();
613
+
614
+ // Main get function
615
+ async function getComponents(componentNames = [], options = {}) {
616
+ try {
617
+ await sharedRegistry.loadRegistry();
618
+ } catch (error) {
619
+ Print.error('Could not load component registry from official repository');
620
+ Print.info('Check your internet connection and try again');
621
+ return false;
622
+ }
623
+
624
+ // Interactive mode if no components specified
625
+ if (!componentNames || componentNames.length === 0) {
626
+ await sharedRegistry.interactiveInstall();
627
+ return true;
628
+ }
629
+
630
+ // Determine category
631
+ const category = options.service ? 'Service' : 'Visual';
632
+
633
+ if (componentNames.length === 1) {
634
+ // Single component install
635
+ const componentInfo = sharedRegistry.findComponentInRegistry(componentNames[0]);
636
+
637
+ if (!componentInfo) {
638
+ Print.error(`Component '${componentNames[0]}' not found in official repository`);
639
+ Print.commandExample('View available components', 'slice browse');
640
+ return false;
641
+ }
642
+
643
+ // Use the category from registry unless Service is explicitly requested
644
+ const actualCategory = options.service ? 'Service' : componentInfo.category;
645
+
646
+ try {
647
+ await sharedRegistry.installComponent(componentInfo.name, actualCategory, options.force);
648
+ return true;
649
+ } catch (error) {
650
+ Print.error(`Error installing component: ${error.message}`);
651
+ return false;
652
+ }
653
+ } else {
654
+ // Multiple components install
655
+ const normalizedComponents = componentNames.map(name =>
656
+ name.charAt(0).toUpperCase() + name.slice(1)
657
+ );
658
+
659
+ try {
660
+ await sharedRegistry.installMultipleComponents(normalizedComponents, category, options.force);
661
+ return true;
662
+ } catch (error) {
663
+ Print.error(`Error installing components: ${error.message}`);
664
+ return false;
665
+ }
666
+ }
667
+ }
668
+
669
+ // List components function
670
+ async function listComponents() {
671
+ try {
672
+ await sharedRegistry.loadRegistry();
673
+ sharedRegistry.displayAvailableComponents();
674
+ return true;
675
+ } catch (error) {
676
+ Print.error('Could not load component registry from official repository');
677
+ Print.info('Check your internet connection and try again');
678
+ return false;
679
+ }
680
+ }
681
+
682
+ // Sync components function
683
+ async function syncComponents(options = {}) {
684
+ try {
685
+ await sharedRegistry.loadRegistry();
686
+ return await sharedRegistry.updateAllComponents(options.force);
687
+ } catch (error) {
688
+ Print.error('Could not load component registry from official repository');
689
+ Print.info('Check your internet connection and try again');
690
+ return false;
691
+ }
692
+ }
693
+
694
+ export default getComponents;
695
+ export { listComponents, syncComponents, ComponentRegistry, loadConfig, runConcurrent, fetchWithRetry };