weloop-kosign 1.1.0 → 1.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/cli-remote.js +482 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weloop-kosign",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "CLI tool for installing Weloop UI components",
5
5
  "keywords": [
6
6
  "weloop",
@@ -33,21 +33,43 @@ const { execSync } = require('child_process');
33
33
  // CONFIGURATION
34
34
  // ============================================================================
35
35
 
36
+ /**
37
+ * Determines the default registry URL to use.
38
+ *
39
+ * Priority:
40
+ * 1. Local registry directory (if running in the component library project)
41
+ * 2. Environment variable WELOOP_REGISTRY_URL
42
+ * 3. Default remote GitLab URL
43
+ *
44
+ * @returns {string} The registry URL or path
45
+ */
36
46
  function getDefaultRegistryUrl() {
37
47
  const localRegistryPath = path.join(__dirname, '../registry');
48
+
49
+ // Check if we're running in the component library project itself
38
50
  if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
39
51
  return localRegistryPath;
40
52
  }
53
+
54
+ // Fall back to environment variable or default remote URL
41
55
  return process.env.WELOOP_REGISTRY_URL ||
42
56
  'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
43
57
  }
44
58
 
45
59
  const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
46
60
 
61
+ // Network timeout constants (in milliseconds)
62
+ const REQUEST_TIMEOUT = 15000;
63
+ const RETRY_DELAY = 1000;
64
+
47
65
  // ============================================================================
48
- // UTILITIES
66
+ // UTILITIES - Logging and File System Helpers
49
67
  // ============================================================================
50
68
 
69
+ /**
70
+ * ANSI color codes for terminal output
71
+ * Makes the CLI output more readable with colored messages
72
+ */
51
73
  const colors = {
52
74
  reset: '\x1b[0m',
53
75
  green: '\x1b[32m',
@@ -57,52 +79,98 @@ const colors = {
57
79
  cyan: '\x1b[36m',
58
80
  };
59
81
 
82
+ /**
83
+ * Logs a message with optional color formatting
84
+ * @param {string} message - The message to log
85
+ * @param {string} color - Color name from colors object
86
+ */
60
87
  function log(message, color = 'reset') {
61
88
  console.log(`${colors[color]}${message}${colors.reset}`);
62
89
  }
63
90
 
91
+ /**
92
+ * Logs an error message in red
93
+ */
64
94
  function error(message) {
65
95
  log(`Error: ${message}`, 'red');
66
96
  }
67
97
 
98
+ /**
99
+ * Logs a success message in green
100
+ */
68
101
  function success(message) {
69
102
  log(message, 'green');
70
103
  }
71
104
 
105
+ /**
106
+ * Logs an informational message in blue
107
+ */
72
108
  function info(message) {
73
109
  log(message, 'blue');
74
110
  }
75
111
 
112
+ /**
113
+ * Logs a warning message in yellow
114
+ */
76
115
  function warn(message) {
77
116
  log(`Warning: ${message}`, 'yellow');
78
117
  }
79
118
 
119
+ /**
120
+ * Creates a directory if it doesn't exist
121
+ * Uses recursive option to create parent directories as needed
122
+ * @param {string} dirPath - Path to the directory
123
+ */
80
124
  function ensureDirectoryExists(dirPath) {
81
125
  if (!fs.existsSync(dirPath)) {
82
126
  fs.mkdirSync(dirPath, { recursive: true });
83
127
  }
84
128
  }
85
129
 
130
+ /**
131
+ * Checks if a path is a local file path or a remote URL
132
+ * @param {string} pathOrUrl - Path or URL to check
133
+ * @returns {boolean} True if it's a local path, false if it's a URL
134
+ */
86
135
  function isLocalPath(pathOrUrl) {
87
136
  return !pathOrUrl.startsWith('http://') && !pathOrUrl.startsWith('https://');
88
137
  }
89
138
 
90
139
  // ============================================================================
91
- // PACKAGE MANAGEMENT
140
+ // PACKAGE MANAGEMENT - Detecting and Installing Dependencies
92
141
  // ============================================================================
93
142
 
143
+ /**
144
+ * Automatically detects which package manager the project is using
145
+ *
146
+ * Detection order:
147
+ * 1. Check for lock files (most reliable)
148
+ * 2. Check npm user agent (fallback)
149
+ * 3. Default to npm
150
+ *
151
+ * @returns {string} Package manager name: 'npm', 'yarn', or 'pnpm'
152
+ */
94
153
  function detectPackageManager() {
154
+ // Check for lock files first (most reliable indicator)
95
155
  if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
96
156
  if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
97
157
  if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
98
158
 
159
+ // Fallback: check npm user agent (set by npm/yarn/pnpm when running)
99
160
  const userAgent = process.env.npm_config_user_agent || '';
100
161
  if (userAgent.includes('yarn')) return 'yarn';
101
162
  if (userAgent.includes('pnpm')) return 'pnpm';
102
163
 
164
+ // Default to npm if we can't detect anything
103
165
  return 'npm';
104
166
  }
105
167
 
168
+ /**
169
+ * Generates the install command for the given package manager
170
+ * @param {string} packageManager - 'npm', 'yarn', or 'pnpm'
171
+ * @param {string[]} packages - Array of package names to install
172
+ * @returns {string} The install command to run
173
+ */
106
174
  function getInstallCommand(packageManager, packages) {
107
175
  const packagesStr = packages.join(' ');
108
176
  switch (packageManager) {
@@ -113,36 +181,63 @@ function getInstallCommand(packageManager, packages) {
113
181
  }
114
182
  }
115
183
 
184
+ /**
185
+ * Checks if a package is already installed in the project
186
+ * Searches both dependencies and devDependencies
187
+ * @param {string} packageName - Name of the package to check
188
+ * @returns {boolean} True if package is installed, false otherwise
189
+ */
116
190
  function checkPackageInstalled(packageName) {
117
191
  const packageJsonPath = path.join(process.cwd(), 'package.json');
118
192
  if (!fs.existsSync(packageJsonPath)) return false;
119
193
 
120
194
  try {
121
195
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
196
+ // Check both dependencies and devDependencies
122
197
  const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
123
198
  return !!allDeps[packageName];
124
199
  } catch (e) {
200
+ // If we can't read/parse package.json, assume package is not installed
125
201
  return false;
126
202
  }
127
203
  }
128
204
 
205
+ /**
206
+ * Filters out dependencies that are already installed
207
+ * Handles scoped packages (e.g., @radix-ui/react-button)
208
+ * @param {string[]} requiredDeps - List of required package names
209
+ * @returns {string[]} List of packages that need to be installed
210
+ */
129
211
  function getMissingDependencies(requiredDeps) {
130
212
  const packageJsonPath = path.join(process.cwd(), 'package.json');
131
- if (!fs.existsSync(packageJsonPath)) return requiredDeps;
213
+ if (!fs.existsSync(packageJsonPath)) {
214
+ // No package.json means we need to install everything
215
+ return requiredDeps;
216
+ }
132
217
 
133
218
  try {
134
219
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
135
220
  const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
136
221
 
137
222
  return requiredDeps.filter(dep => {
223
+ // For scoped packages like @radix-ui/react-button, also check @radix-ui
138
224
  const depName = dep.split('/').slice(0, 2).join('/');
139
225
  return !allDeps[dep] && !allDeps[depName];
140
226
  });
141
227
  } catch (e) {
228
+ // If we can't parse package.json, assume all deps are missing
142
229
  return requiredDeps;
143
230
  }
144
231
  }
145
232
 
233
+ /**
234
+ * Installs npm packages using the detected package manager
235
+ *
236
+ * For npm: Filters out ERESOLVE peer dependency warnings (common false positives)
237
+ * For yarn/pnpm: Uses standard execSync
238
+ *
239
+ * @param {string[]} packages - Array of package names to install
240
+ */
146
241
  async function installPackages(packages) {
147
242
  if (packages.length === 0) return;
148
243
 
@@ -152,7 +247,8 @@ async function installPackages(packages) {
152
247
  try {
153
248
  const installCmd = getInstallCommand(packageManager, packages);
154
249
 
155
- // Filter out ERESOLVE peer dependency warnings while keeping errors
250
+ // npm has noisy ERESOLVE warnings that aren't real errors
251
+ // We filter these out while keeping actual error messages
156
252
  if (packageManager === 'npm') {
157
253
  const { spawn } = require('child_process');
158
254
  const [cmd, ...args] = installCmd.split(' ');
@@ -167,7 +263,8 @@ async function installPackages(packages) {
167
263
  let stderr = '';
168
264
  proc.stderr.on('data', (data) => {
169
265
  const output = data.toString();
170
- // Filter out ERESOLVE warnings but keep actual errors
266
+ // Filter out ERESOLVE peer dependency warnings (these are usually safe to ignore)
267
+ // but keep actual error messages visible to the user
171
268
  if (!output.includes('ERESOLVE overriding peer dependency') &&
172
269
  !output.includes('npm warn ERESOLVE')) {
173
270
  process.stderr.write(data);
@@ -177,13 +274,14 @@ async function installPackages(packages) {
177
274
 
178
275
  proc.on('close', (code) => {
179
276
  if (code !== 0) {
180
- // Check if it's a real error (not just warnings)
277
+ // npm sometimes exits with non-zero code due to warnings, not real errors
278
+ // Check if there's an actual error message (not just ERESOLVE warnings)
181
279
  const hasRealError = stderr.includes('npm error') &&
182
280
  !stderr.match(/npm error.*ERESOLVE/);
183
281
  if (hasRealError) {
184
282
  reject(new Error(`Installation failed with code ${code}`));
185
283
  } else {
186
- // Installation succeeded despite warnings
284
+ // Installation succeeded despite warnings - this is common with peer deps
187
285
  resolve();
188
286
  }
189
287
  } else {
@@ -206,13 +304,22 @@ async function installPackages(packages) {
206
304
  }
207
305
 
208
306
  // ============================================================================
209
- // FILE OPERATIONS
307
+ // FILE OPERATIONS - Configuration and File Handling
210
308
  // ============================================================================
211
309
 
310
+ /**
311
+ * Creates a default components.json configuration file
312
+ *
313
+ * Auto-detects Next.js app directory structure and sets appropriate paths.
314
+ * This file stores project-specific settings like component paths and aliases.
315
+ *
316
+ * @returns {object} The default configuration object
317
+ */
212
318
  function createDefaultComponentsConfig() {
213
319
  const configPath = path.join(process.cwd(), 'components.json');
214
320
 
215
- // Detect if it's a Next.js app (app directory) or pages directory
321
+ // Detect Next.js project structure
322
+ // App Router uses 'app/globals.css', Pages Router uses 'styles/globals.css'
216
323
  const hasAppDir = fs.existsSync(path.join(process.cwd(), 'app'));
217
324
  const cssPath = hasAppDir ? 'app/globals.css' : 'styles/globals.css';
218
325
 
@@ -245,6 +352,12 @@ function createDefaultComponentsConfig() {
245
352
  return defaultConfig;
246
353
  }
247
354
 
355
+ /**
356
+ * Loads the components.json configuration file
357
+ * Creates a default one if it doesn't exist
358
+ *
359
+ * @returns {object} The configuration object
360
+ */
248
361
  function loadComponentsConfig() {
249
362
  const configPath = path.join(process.cwd(), 'components.json');
250
363
 
@@ -257,10 +370,25 @@ function loadComponentsConfig() {
257
370
  return JSON.parse(content);
258
371
  }
259
372
 
373
+ /**
374
+ * Fetches and parses a JSON file from a local path or remote URL
375
+ *
376
+ * Features:
377
+ * - Handles local files and remote URLs
378
+ * - Automatic retry on network errors
379
+ * - Follows HTTP redirects
380
+ * - Provides helpful error messages
381
+ *
382
+ * @param {string} urlOrPath - Local file path or remote URL
383
+ * @param {number} retries - Number of retry attempts (default: 3)
384
+ * @returns {Promise<object>} Parsed JSON object
385
+ */
260
386
  async function fetchJSON(urlOrPath, retries = 3) {
387
+ // Handle local file paths
261
388
  if (isLocalPath(urlOrPath)) {
262
389
  return new Promise((resolve, reject) => {
263
390
  try {
391
+ // Resolve relative paths to absolute
264
392
  const fullPath = path.isAbsolute(urlOrPath)
265
393
  ? urlOrPath
266
394
  : path.join(process.cwd(), urlOrPath);
@@ -278,6 +406,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
278
406
  });
279
407
  }
280
408
 
409
+ // Handle remote URLs
281
410
  return new Promise((resolve, reject) => {
282
411
  const client = urlOrPath.startsWith('https') ? https : http;
283
412
  let timeout;
@@ -287,10 +416,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
287
416
  request = client.get(urlOrPath, (res) => {
288
417
  clearTimeout(timeout);
289
418
 
419
+ // Handle HTTP redirects (301, 302)
290
420
  if (res.statusCode === 302 || res.statusCode === 301) {
291
421
  return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
292
422
  }
293
423
 
424
+ // Provide helpful error messages for common HTTP status codes
294
425
  if (res.statusCode === 403) {
295
426
  reject(new Error(`Access forbidden (403). Repository may be private. Make it public in GitLab/GitHub settings, or use --registry with a public URL.`));
296
427
  return;
@@ -306,10 +437,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
306
437
  return;
307
438
  }
308
439
 
440
+ // Collect response data
309
441
  let data = '';
310
442
  res.on('data', (chunk) => { data += chunk; });
311
443
  res.on('end', () => {
312
444
  try {
445
+ // Check if we got HTML instead of JSON (common when repo is private)
313
446
  if (data.trim().startsWith('<')) {
314
447
  reject(new Error('Received HTML instead of JSON. Repository may be private or URL incorrect.'));
315
448
  return;
@@ -321,36 +454,46 @@ async function fetchJSON(urlOrPath, retries = 3) {
321
454
  });
322
455
  });
323
456
 
457
+ // Handle network errors with automatic retry
324
458
  request.on('error', (err) => {
325
459
  clearTimeout(timeout);
326
460
  if (retries > 0 && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND')) {
327
461
  info(`Connection error, retrying... (${retries} attempts left)`);
328
462
  setTimeout(() => {
329
463
  fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
330
- }, 1000);
464
+ }, RETRY_DELAY);
331
465
  } else {
332
466
  reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
333
467
  }
334
468
  });
335
469
 
470
+ // Set request timeout with retry logic
336
471
  timeout = setTimeout(() => {
337
472
  request.destroy();
338
473
  if (retries > 0) {
339
474
  info(`Request timeout, retrying... (${retries} attempts left)`);
340
475
  setTimeout(() => {
341
476
  fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
342
- }, 1000);
477
+ }, RETRY_DELAY);
343
478
  } else {
344
479
  reject(new Error('Request timeout after multiple attempts'));
345
480
  }
346
- }, 15000);
481
+ }, REQUEST_TIMEOUT);
347
482
  };
348
483
 
349
484
  makeRequest();
350
485
  });
351
486
  }
352
487
 
488
+ /**
489
+ * Fetches text content (like CSS) from a local path or remote URL
490
+ * Similar to fetchJSON but returns raw text instead of parsing JSON
491
+ *
492
+ * @param {string} urlOrPath - Local file path or remote URL
493
+ * @returns {Promise<string>} The file content as text
494
+ */
353
495
  async function fetchText(urlOrPath) {
496
+ // Handle local file paths
354
497
  if (isLocalPath(urlOrPath)) {
355
498
  if (!fs.existsSync(urlOrPath)) {
356
499
  throw new Error(`File not found: ${urlOrPath}`);
@@ -358,15 +501,18 @@ async function fetchText(urlOrPath) {
358
501
  return fs.readFileSync(urlOrPath, 'utf-8');
359
502
  }
360
503
 
504
+ // Handle remote URLs
361
505
  return new Promise((resolve, reject) => {
362
506
  const client = urlOrPath.startsWith('https') ? https : http;
363
507
  let data = '';
364
508
 
365
509
  const req = client.get(urlOrPath, (res) => {
510
+ // Follow redirects
366
511
  if (res.statusCode === 302 || res.statusCode === 301) {
367
512
  return fetchText(res.headers.location).then(resolve).catch(reject);
368
513
  }
369
514
 
515
+ // Handle common HTTP errors
370
516
  if (res.statusCode === 403) {
371
517
  reject(new Error('Access forbidden - repository may be private'));
372
518
  return;
@@ -382,8 +528,10 @@ async function fetchText(urlOrPath) {
382
528
  return;
383
529
  }
384
530
 
531
+ // Collect response data
385
532
  res.on('data', (chunk) => { data += chunk; });
386
533
  res.on('end', () => {
534
+ // Check if we got HTML instead of CSS (common when repo is private)
387
535
  if (data.trim().startsWith('<')) {
388
536
  reject(new Error('Received HTML instead of CSS'));
389
537
  return;
@@ -392,42 +540,70 @@ async function fetchText(urlOrPath) {
392
540
  });
393
541
  });
394
542
 
543
+ // Handle network errors with automatic retry
395
544
  req.on('error', (err) => {
396
545
  if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
397
546
  setTimeout(() => {
398
547
  fetchText(urlOrPath).then(resolve).catch(reject);
399
- }, 1000);
548
+ }, RETRY_DELAY);
400
549
  } else {
401
550
  reject(err);
402
551
  }
403
552
  });
404
553
 
405
- req.setTimeout(15000, () => {
554
+ // Set request timeout
555
+ req.setTimeout(REQUEST_TIMEOUT, () => {
406
556
  req.destroy();
407
557
  reject(new Error('Request timeout'));
408
558
  });
409
559
  });
410
560
  }
411
561
 
562
+ /**
563
+ * Resolves the full path/URL for a registry file
564
+ * Handles both local paths and remote URLs
565
+ *
566
+ * @param {string} baseUrl - Base registry URL or path
567
+ * @param {string} fileName - Name of the file to resolve
568
+ * @returns {string} Full path or URL to the file
569
+ */
412
570
  function resolveRegistryPath(baseUrl, fileName) {
413
571
  if (isLocalPath(baseUrl)) {
572
+ // Resolve local paths (absolute or relative)
414
573
  const basePath = path.isAbsolute(baseUrl)
415
574
  ? baseUrl
416
575
  : path.join(process.cwd(), baseUrl);
417
576
  return path.join(basePath, fileName);
418
577
  }
578
+ // For remote URLs, just append the filename
419
579
  return `${baseUrl}/${fileName}`;
420
580
  }
421
581
 
582
+ /**
583
+ * Loads a component registry file from the registry
584
+ * Returns null if component not found (doesn't throw)
585
+ *
586
+ * @param {string} componentName - Name of the component
587
+ * @param {string} registryBaseUrl - Base URL/path of the registry
588
+ * @returns {Promise<object|null>} Component registry object or null if not found
589
+ */
422
590
  async function loadRegistry(componentName, registryBaseUrl) {
423
591
  try {
424
592
  const registryPath = resolveRegistryPath(registryBaseUrl, `${componentName}.json`);
425
593
  return await fetchJSON(registryPath);
426
594
  } catch (err) {
595
+ // Component not found - return null instead of throwing
427
596
  return null;
428
597
  }
429
598
  }
430
599
 
600
+ /**
601
+ * Loads the registry index file (lists all available components)
602
+ * Throws an error if index cannot be loaded
603
+ *
604
+ * @param {string} registryBaseUrl - Base URL/path of the registry
605
+ * @returns {Promise<object>} Index object containing list of components
606
+ */
431
607
  async function loadIndex(registryBaseUrl) {
432
608
  const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
433
609
  try {
@@ -439,31 +615,47 @@ async function loadIndex(registryBaseUrl) {
439
615
  }
440
616
 
441
617
  // ============================================================================
442
- // CSS PROCESSING
618
+ // CSS PROCESSING - Style Installation and Formatting
443
619
  // ============================================================================
444
620
 
621
+ /**
622
+ * Checks if CSS content contains Weloop design system variables
623
+ * Used to detect if styles have already been installed
624
+ *
625
+ * @param {string} content - CSS content to check
626
+ * @returns {boolean} True if Weloop styles are present
627
+ */
445
628
  function hasWeloopStyles(content) {
446
- return content.includes('--WLDS-PRM-') ||
447
- content.includes('--WLDS-RED-') ||
448
- content.includes('--WLDS-NTL-') ||
449
- content.includes('--system-100') ||
629
+ // Check for Weloop-specific CSS variable prefixes
630
+ return content.includes('--WLDS-PRM-') || // Primary colors
631
+ content.includes('--WLDS-RED-') || // Red colors
632
+ content.includes('--WLDS-NTL-') || // Neutral colors
633
+ content.includes('--system-100') || // System colors
450
634
  content.includes('--system-200');
451
635
  }
452
636
 
637
+ /**
638
+ * Removes duplicate tw-animate-css imports and keeps only one
639
+ * Ensures the import appears right after tailwindcss import
640
+ *
641
+ * @param {string} cssContent - CSS content to clean
642
+ * @returns {string} CSS content with duplicates removed
643
+ */
453
644
  function removeDuplicateTwAnimateImports(cssContent) {
454
645
  const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
455
646
  const matches = cssContent.match(twAnimatePattern);
456
647
 
457
648
  if (matches && matches.length > 1) {
458
- // Remove all occurrences
649
+ // Remove all occurrences first
459
650
  let cleaned = cssContent.replace(twAnimatePattern, '');
460
- // Add it back once after tailwindcss import
651
+ // Add it back once, right after tailwindcss import if it exists
461
652
  if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
462
653
  cleaned = cleaned.replace(
463
654
  /(@import\s+["']tailwindcss["'];?\s*\n?)/,
464
655
  '$1@import "tw-animate-css";\n'
465
656
  );
466
657
  } else {
658
+ // If no tailwindcss import, add it at the beginning
467
659
  cleaned = '@import "tw-animate-css";\n' + cleaned;
468
660
  }
469
661
  return cleaned;
@@ -472,11 +664,19 @@ function removeDuplicateTwAnimateImports(cssContent) {
472
664
  return cssContent;
473
665
  }
474
666
 
667
+ /**
668
+ * Removes duplicate @custom-variant declarations
669
+ * Keeps only one instance at the top
670
+ *
671
+ * @param {string} cssContent - CSS content to clean
672
+ * @returns {string} CSS content with duplicates removed
673
+ */
475
674
  function removeDuplicateCustomVariant(cssContent) {
476
675
  const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
477
676
  const matches = cssContent.match(variantPattern);
478
677
 
479
678
  if (matches && matches.length > 1) {
679
+ // Remove all occurrences and add back one at the top
480
680
  const withoutVariants = cssContent.replace(variantPattern, '');
481
681
  return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
482
682
  }
@@ -484,11 +684,93 @@ function removeDuplicateCustomVariant(cssContent) {
484
684
  return cssContent;
485
685
  }
486
686
 
687
+ /**
688
+ * Normalizes CSS format to ensure consistent structure
689
+ *
690
+ * Format order:
691
+ * 1. @import statements (tailwindcss, then tw-animate-css, then others)
692
+ * 2. @custom-variant declaration
693
+ * 3. Rest of the CSS content
694
+ *
695
+ * This ensures the CSS file always has the same structure regardless of
696
+ * how it was generated or merged.
697
+ *
698
+ * @param {string} cssContent - CSS content to normalize
699
+ * @returns {string} Normalized CSS content
700
+ */
701
+ function normalizeCSSFormat(cssContent) {
702
+ // Extract @custom-variant declaration
703
+ const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
704
+ const variantMatch = cssContent.match(variantPattern);
705
+ const hasVariant = variantMatch && variantMatch.length > 0;
706
+
707
+ // Extract all @import statements
708
+ const importPattern = /@import\s+["'][^"']+["'];?\s*\n?/g;
709
+ const imports = cssContent.match(importPattern) || [];
710
+
711
+ // Separate imports by type for proper ordering
712
+ const tailwindImport = imports.find(imp => imp.includes('tailwindcss'));
713
+ const twAnimateImport = imports.find(imp => imp.includes('tw-animate-css'));
714
+ const otherImports = imports.filter(imp =>
715
+ !imp.includes('tailwindcss') && !imp.includes('tw-animate-css')
716
+ );
717
+
718
+ // Remove all imports and variant from content to get the rest
719
+ let content = cssContent
720
+ .replace(variantPattern, '')
721
+ .replace(importPattern, '')
722
+ .trim();
723
+
724
+ // Build normalized format in the correct order
725
+ let normalized = '';
726
+
727
+ // Step 1: Imports first (tailwindcss, then tw-animate-css, then others)
728
+ if (tailwindImport) {
729
+ normalized += tailwindImport.trim() + '\n';
730
+ }
731
+ if (twAnimateImport) {
732
+ normalized += twAnimateImport.trim() + '\n';
733
+ }
734
+ if (otherImports.length > 0) {
735
+ normalized += otherImports.join('') + '\n';
736
+ }
737
+
738
+ // Step 2: @custom-variant second (if it exists)
739
+ if (hasVariant) {
740
+ if (normalized) {
741
+ normalized += '\n@custom-variant dark (&:is(.dark *));\n';
742
+ } else {
743
+ normalized += '@custom-variant dark (&:is(.dark *));\n';
744
+ }
745
+ }
746
+
747
+ // Step 3: Rest of content (theme variables, etc.)
748
+ if (normalized && content) {
749
+ normalized += '\n' + content;
750
+ } else if (content) {
751
+ normalized = content;
752
+ }
753
+
754
+ return normalized.trim() + (normalized ? '\n' : '');
755
+ }
756
+
757
+ /**
758
+ * Processes tw-animate-css import based on whether the package is installed
759
+ *
760
+ * - If package is NOT installed: Removes the import to prevent build errors
761
+ * - If package IS installed: Ensures import exists and removes duplicates
762
+ *
763
+ * @param {string} cssContent - CSS content to process
764
+ * @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
765
+ * @param {boolean} forceUpdate - Whether this is a forced update
766
+ * @returns {string} Processed CSS content
767
+ */
487
768
  function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
488
769
  const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
489
770
  let processed = cssContent;
490
771
 
491
772
  if (!hasTwAnimate) {
773
+ // Package not installed - remove import to prevent build errors
492
774
  processed = cssContent.replace(twAnimatePattern, '');
493
775
  if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
494
776
  if (forceUpdate) {
@@ -502,10 +784,10 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
502
784
  }
503
785
  }
504
786
  } else {
505
- // Remove any duplicates first
787
+ // Package is installed - ensure import exists and remove duplicates
506
788
  processed = removeDuplicateTwAnimateImports(processed);
507
789
 
508
- // Ensure import exists if package is installed (only if not already present)
790
+ // Add import if it doesn't exist (right after tailwindcss if present)
509
791
  if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
510
792
  if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
511
793
  processed = processed.replace(
@@ -521,10 +803,25 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
521
803
  return processed;
522
804
  }
523
805
 
806
+ /**
807
+ * Removes tailwindcss import statements from CSS
808
+ * Used when merging styles to avoid duplicates
809
+ *
810
+ * @param {string} cssContent - CSS content
811
+ * @returns {string} CSS content without tailwindcss imports
812
+ */
524
813
  function removeTailwindImport(cssContent) {
525
814
  return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
526
815
  }
527
816
 
817
+ /**
818
+ * Ensures tw-animate-css import exists if the package is installed
819
+ * Adds it right after tailwindcss import if present
820
+ *
821
+ * @param {string} cssContent - CSS content
822
+ * @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
823
+ * @returns {string} CSS content with tw-animate-css import ensured
824
+ */
528
825
  function ensureTwAnimateImport(cssContent, hasTwAnimate) {
529
826
  if (!hasTwAnimate) {
530
827
  return cssContent;
@@ -538,7 +835,7 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
538
835
  return cleaned;
539
836
  }
540
837
 
541
- // Add import after tailwindcss if it exists
838
+ // Add import after tailwindcss if it exists, otherwise at the beginning
542
839
  if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
543
840
  return cleaned.replace(
544
841
  /(@import\s+["']tailwindcss["'];?\s*\n?)/,
@@ -549,21 +846,36 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
549
846
  return '@import "tw-animate-css";\n' + cleaned;
550
847
  }
551
848
 
849
+ /**
850
+ * Merges Weloop styles with existing CSS that has Tailwind imports
851
+ *
852
+ * Strategy:
853
+ * - Finds the tailwindcss import in existing CSS
854
+ * - Inserts Weloop styles right after it
855
+ * - Handles tw-animate-css import to avoid duplicates
856
+ *
857
+ * @param {string} existing - Existing CSS content
858
+ * @param {string} weloopStyles - Weloop styles to merge in
859
+ * @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
860
+ * @returns {string} Merged CSS content
861
+ */
552
862
  function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
553
863
  const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
554
864
  if (!tailwindMatch) {
865
+ // No tailwindcss import found - just prepend Weloop styles
555
866
  return weloopStyles + '\n\n' + existing;
556
867
  }
557
868
 
869
+ // Split existing CSS around the tailwindcss import
558
870
  const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
559
871
  const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
560
872
 
561
- // Check if tw-animate-css import already exists in existing or weloopStyles
873
+ // Check if tw-animate-css import already exists
562
874
  const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
563
875
  const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
564
876
 
565
- // If the existing file already has the tw-animate import, strip it from the
566
- // Weloop styles to avoid duplicating it in the merge output.
877
+ // If existing file already has tw-animate import, remove it from Weloop styles
878
+ // to avoid duplicating it in the merge output
567
879
  let weloopStylesCleaned = weloopStyles;
568
880
  if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
569
881
  weloopStylesCleaned = weloopStyles.replace(
@@ -572,44 +884,75 @@ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
572
884
  );
573
885
  }
574
886
 
887
+ // Add tw-animate import if needed (package installed but import missing)
575
888
  let importsToAdd = '';
576
889
  if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
577
890
  importsToAdd = '@import "tw-animate-css";\n';
578
891
  }
579
892
 
893
+ // Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
580
894
  return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
581
895
  }
582
896
 
897
+ /**
898
+ * Replaces existing Weloop styles with new ones
899
+ *
900
+ * Finds where Weloop styles start (usually @theme inline or :root)
901
+ * and replaces everything from there with the new styles.
902
+ * Preserves imports that come before the Weloop styles.
903
+ *
904
+ * @param {string} existing - Existing CSS content
905
+ * @param {string} newStyles - New Weloop styles to replace with
906
+ * @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
907
+ * @returns {string} CSS content with Weloop styles replaced
908
+ */
583
909
  function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
584
910
  const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
585
911
  const imports = existing.match(importPattern) || [];
586
912
  const importsText = imports.join('');
587
913
 
914
+ // Find where Weloop styles start (look for @theme inline or :root)
588
915
  const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
589
916
  const weloopStartMatch = existing.search(weloopStartPattern);
590
917
 
591
918
  if (weloopStartMatch === -1) {
919
+ // No existing Weloop styles found - just append
592
920
  return existing + '\n\n' + newStyles;
593
921
  }
594
922
 
923
+ // Extract content before Weloop styles
595
924
  let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
925
+ // Keep non-Weloop imports (filter out tw-animate if package not installed)
596
926
  const nonWeloopImports = importsText.split('\n').filter(imp =>
597
927
  !imp.includes('tw-animate-css') || hasTwAnimate
598
928
  ).join('\n');
929
+ // Remove all imports from contentBeforeWeloop (we'll add them back)
599
930
  contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
600
931
 
932
+ // Return: preserved imports + content before Weloop + new Weloop styles
601
933
  return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
602
934
  }
603
935
 
936
+ /**
937
+ * Fetches the CSS file from the registry
938
+ *
939
+ * For local paths: Looks for app/globals.css relative to registry directory
940
+ * For remote URLs: Constructs URL by replacing /registry with /app/globals.css
941
+ *
942
+ * @param {string} registryUrl - Registry base URL or path
943
+ * @returns {Promise<string>} CSS file content
944
+ */
604
945
  async function fetchCSSFromRegistry(registryUrl) {
605
946
  let sourceCssPath;
606
947
 
607
948
  if (isLocalPath(registryUrl)) {
949
+ // Local path: find app/globals.css relative to registry directory
608
950
  const basePath = path.isAbsolute(registryUrl)
609
951
  ? path.dirname(registryUrl)
610
952
  : path.join(process.cwd(), path.dirname(registryUrl));
611
953
  sourceCssPath = path.join(basePath, 'app', 'globals.css');
612
954
  } else {
955
+ // Remote URL: replace /registry with /app/globals.css
613
956
  const baseUrl = registryUrl.replace('/registry', '');
614
957
  sourceCssPath = `${baseUrl}/app/globals.css`;
615
958
  }
@@ -617,17 +960,31 @@ async function fetchCSSFromRegistry(registryUrl) {
617
960
  return await fetchText(sourceCssPath);
618
961
  }
619
962
 
963
+ /**
964
+ * Installs or updates CSS styles from the registry
965
+ *
966
+ * Handles three scenarios:
967
+ * 1. --overwrite: Replaces entire file
968
+ * 2. Normal update: Intelligently merges with existing styles
969
+ * 3. New file: Creates file with Weloop styles
970
+ *
971
+ * @param {object} config - Components configuration object
972
+ * @param {string} registryUrl - Registry base URL or path
973
+ * @param {boolean} forceUpdate - Whether to overwrite existing file
974
+ * @param {boolean} silent - Whether to suppress output messages
975
+ */
620
976
  async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
621
977
  const cssPath = config.tailwind?.css || 'app/globals.css';
622
978
  const fullCssPath = path.join(process.cwd(), cssPath);
623
979
 
624
- // Check if Weloop styles already exist
980
+ // Check if Weloop styles already exist in the file
625
981
  let hasWeloopStylesInFile = false;
626
982
  if (fs.existsSync(fullCssPath)) {
627
983
  const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
628
984
  hasWeloopStylesInFile = hasWeloopStyles(existingContent);
629
985
 
630
- // If styles already exist and not forcing update, skip silently
986
+ // If styles already exist and we're not forcing update, skip silently
987
+ // This prevents unnecessary updates when installing components
631
988
  if (hasWeloopStylesInFile && !forceUpdate && silent) {
632
989
  return;
633
990
  }
@@ -644,10 +1001,13 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
644
1001
  const hasTwAnimate = checkPackageInstalled('tw-animate-css');
645
1002
  let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
646
1003
 
647
- // Handle file installation
1004
+ // Handle file installation based on mode and existing file state
648
1005
  if (forceUpdate && fs.existsSync(fullCssPath)) {
649
- // --overwrite: Replace entire file
650
- fs.writeFileSync(fullCssPath, processedCssContent);
1006
+ // Mode 1: --overwrite flag - Replace entire file with fresh styles
1007
+ let normalized = normalizeCSSFormat(processedCssContent);
1008
+ normalized = removeDuplicateTwAnimateImports(normalized);
1009
+ normalized = removeDuplicateCustomVariant(normalized);
1010
+ fs.writeFileSync(fullCssPath, normalized);
651
1011
  if (!silent) {
652
1012
  success(`Overwritten ${cssPath} with Weloop styles`);
653
1013
  if (hasTwAnimate) {
@@ -655,34 +1015,36 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
655
1015
  }
656
1016
  }
657
1017
  } else if (fs.existsSync(fullCssPath)) {
658
- // Normal mode: Merge intelligently
1018
+ // Mode 2: Normal update - Intelligently merge with existing styles
659
1019
  const existing = fs.readFileSync(fullCssPath, 'utf-8');
660
1020
  const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
661
1021
  existing.includes('@tailwind base');
662
1022
 
663
1023
  if (hasWeloopStylesInFile) {
664
- // Replace existing Weloop styles (only if different)
1024
+ // Case 2a: Weloop styles already exist - replace them with updated versions
665
1025
  let weloopStyles = removeTailwindImport(processedCssContent);
666
1026
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
667
1027
  let finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
668
1028
  finalContent = removeDuplicateTwAnimateImports(finalContent);
669
1029
  finalContent = removeDuplicateCustomVariant(finalContent);
1030
+ finalContent = normalizeCSSFormat(finalContent);
670
1031
 
671
- // Only update if content actually changed
1032
+ // Only write if content actually changed (prevents unnecessary file updates)
672
1033
  if (finalContent !== existing) {
673
1034
  fs.writeFileSync(fullCssPath, finalContent);
674
1035
  if (!silent) {
675
1036
  success(`Updated ${cssPath} with Weloop styles`);
676
1037
  }
677
1038
  }
678
- // If no changes, silently skip
1039
+ // If no changes, silently skip (file is already up to date)
679
1040
  } else if (hasTailwindImport) {
680
- // Merge after Tailwind imports
1041
+ // Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
681
1042
  let weloopStyles = removeTailwindImport(processedCssContent);
682
1043
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
683
1044
  let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
684
1045
  merged = removeDuplicateTwAnimateImports(merged);
685
1046
  merged = removeDuplicateCustomVariant(merged);
1047
+ merged = normalizeCSSFormat(merged);
686
1048
  fs.writeFileSync(fullCssPath, merged);
687
1049
  if (!silent) {
688
1050
  success(`Updated ${cssPath} with Weloop styles`);
@@ -692,9 +1054,10 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
692
1054
  info(` Your existing styles are preserved`);
693
1055
  }
694
1056
  } else {
695
- // No Tailwind imports, prepend everything
1057
+ // Case 2c: No Tailwind imports - prepend Weloop styles to existing content
696
1058
  let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
697
1059
 
1060
+ // Ensure tw-animate import exists if package is installed
698
1061
  if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
699
1062
  if (finalCssContent.includes('@import "tailwindcss"')) {
700
1063
  finalCssContent = finalCssContent.replace(
@@ -705,7 +1068,9 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
705
1068
  finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
706
1069
  }
707
1070
  }
708
- fs.writeFileSync(fullCssPath, finalCssContent + '\n\n' + existing);
1071
+ let combined = finalCssContent + '\n\n' + existing;
1072
+ combined = normalizeCSSFormat(combined);
1073
+ fs.writeFileSync(fullCssPath, combined);
709
1074
  if (!silent) {
710
1075
  success(`Updated ${cssPath} with Weloop styles`);
711
1076
  if (hasTwAnimate) {
@@ -714,8 +1079,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
714
1079
  }
715
1080
  }
716
1081
  } else {
717
- // Create new file
718
- fs.writeFileSync(fullCssPath, processedCssContent);
1082
+ // Mode 3: File doesn't exist - create new file with Weloop styles
1083
+ let normalized = normalizeCSSFormat(processedCssContent);
1084
+ normalized = removeDuplicateTwAnimateImports(normalized);
1085
+ normalized = removeDuplicateCustomVariant(normalized);
1086
+ fs.writeFileSync(fullCssPath, normalized);
719
1087
  if (!silent) {
720
1088
  success(`Created ${cssPath} with Weloop styles`);
721
1089
  if (hasTwAnimate) {
@@ -735,13 +1103,22 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
735
1103
  }
736
1104
 
737
1105
  // ============================================================================
738
- // COMPONENT INSTALLATION
1106
+ // COMPONENT INSTALLATION - Installing Components from Registry
739
1107
  // ============================================================================
740
1108
 
1109
+ /**
1110
+ * Creates the utils.ts file if it doesn't exist
1111
+ *
1112
+ * This file contains the 'cn' utility function used by all components
1113
+ * for merging Tailwind classes. It's required for components to work.
1114
+ *
1115
+ * @param {string} utilsPath - Path where utils.ts should be created
1116
+ */
741
1117
  function createUtilsFile(utilsPath) {
742
1118
  if (fs.existsSync(utilsPath)) return;
743
1119
 
744
1120
  // Silently create utils file (matching shadcn/ui behavior)
1121
+ // Don't overwrite if user has customized it
745
1122
  ensureDirectoryExists(path.dirname(utilsPath));
746
1123
 
747
1124
  const utilsContent = `import { clsx, type ClassValue } from "clsx"
@@ -754,10 +1131,27 @@ export function cn(...inputs: ClassValue[]) {
754
1131
  fs.writeFileSync(utilsPath, utilsContent);
755
1132
  }
756
1133
 
1134
+ /**
1135
+ * Installs a component from the registry
1136
+ *
1137
+ * Installation process:
1138
+ * 1. Install base dependencies (clsx, tailwind-merge)
1139
+ * 2. Install tw-animate-css for animations
1140
+ * 3. Install/update CSS styles (silently if already installed)
1141
+ * 4. Install component dependencies (recursive)
1142
+ * 5. Install component files
1143
+ * 6. Create utils.ts if needed
1144
+ * 7. Install npm dependencies
1145
+ *
1146
+ * @param {string} componentName - Name of the component to install
1147
+ * @param {object} options - Installation options
1148
+ * @param {boolean} options.overwrite - Whether to overwrite existing files
1149
+ * @param {string} options.registryUrl - Registry URL or path
1150
+ */
757
1151
  async function installComponent(componentName, options = {}) {
758
1152
  const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
759
1153
 
760
- // Install base dependencies first (required for utils.ts)
1154
+ // Step 1: Install base dependencies (required for utils.ts to work)
761
1155
  const baseDeps = [];
762
1156
  if (!checkPackageInstalled('clsx')) {
763
1157
  baseDeps.push('clsx');
@@ -770,19 +1164,20 @@ async function installComponent(componentName, options = {}) {
770
1164
  await installPackages(baseDeps);
771
1165
  }
772
1166
 
773
- // Load or create components.json (after base deps are installed)
1167
+ // Step 2: Load or create components.json configuration
774
1168
  const config = loadComponentsConfig();
775
1169
 
776
- // Install tw-animate-css automatically (required for animations)
1170
+ // Step 3: Install tw-animate-css automatically (required for component animations)
777
1171
  if (!checkPackageInstalled('tw-animate-css')) {
778
1172
  info('Installing tw-animate-css for animations...');
779
1173
  await installPackages(['tw-animate-css']);
780
1174
  }
781
1175
 
782
- // Install CSS styles early (before component installation) - silent if already installed
1176
+ // Step 4: Install CSS styles early (before component installation)
1177
+ // Silent mode prevents output when styles are already installed
783
1178
  await installCSSStyles(config, registryUrl, false, true);
784
1179
 
785
- // Get paths from components.json
1180
+ // Step 5: Resolve paths from components.json configuration
786
1181
  const uiAlias = config.aliases?.ui || '@/components/ui';
787
1182
  const utilsAlias = config.aliases?.utils || '@/lib/utils';
788
1183
 
@@ -793,30 +1188,34 @@ async function installComponent(componentName, options = {}) {
793
1188
  }
794
1189
  utilsPath = path.join(process.cwd(), utilsPath);
795
1190
 
796
- // Load component registry
1191
+ // Step 6: Load component registry from remote or local source
797
1192
  const registry = await loadRegistry(componentName, registryUrl);
798
1193
 
799
1194
  if (!registry) {
800
1195
  error(`Component "${componentName}" not found in registry.`);
801
1196
  info('Available components:');
802
1197
  try {
1198
+ // Show available components to help user
803
1199
  const index = await loadIndex(registryUrl);
804
1200
  index.registry.forEach(comp => {
805
1201
  console.log(` - ${comp.name}`);
806
1202
  });
807
1203
  } catch (e) {
808
- // Ignore if index fails
1204
+ // Ignore if index fails - we already showed the error
809
1205
  }
810
1206
  process.exit(1);
811
1207
  }
812
1208
 
813
- // Check if component already exists (silently skip if exists, matching shadcn/ui behavior)
1209
+ // Step 7: Check if component already exists
1210
+ // Silently skip if exists (matching shadcn/ui behavior)
1211
+ // Only install if overwrite flag is set
814
1212
  const componentPath = path.join(componentsDir, `${componentName}.tsx`);
815
1213
  if (fs.existsSync(componentPath) && !overwrite) {
816
1214
  return;
817
1215
  }
818
1216
 
819
- // Install registry dependencies first
1217
+ // Step 8: Install component dependencies first (recursive)
1218
+ // Some components depend on other components (e.g., button-group depends on button)
820
1219
  if (registry.registryDependencies && registry.registryDependencies.length > 0) {
821
1220
  info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
822
1221
  for (const dep of registry.registryDependencies) {
@@ -824,7 +1223,8 @@ async function installComponent(componentName, options = {}) {
824
1223
  }
825
1224
  }
826
1225
 
827
- // Install component files
1226
+ // Step 9: Install component files
1227
+ // Components can have multiple files (e.g., component + types + styles)
828
1228
  for (const file of registry.files) {
829
1229
  const filePath = path.join(process.cwd(), file.path);
830
1230
  ensureDirectoryExists(path.dirname(filePath));
@@ -832,10 +1232,11 @@ async function installComponent(componentName, options = {}) {
832
1232
  success(`Installed: ${file.path}`);
833
1233
  }
834
1234
 
835
- // Create utils.ts if needed
1235
+ // Step 10: Create utils.ts if it doesn't exist
1236
+ // This is required for all components to work
836
1237
  createUtilsFile(utilsPath);
837
1238
 
838
- // Install npm dependencies
1239
+ // Step 11: Install npm dependencies required by the component
839
1240
  if (registry.dependencies && registry.dependencies.length > 0) {
840
1241
  const missingDeps = getMissingDependencies(registry.dependencies);
841
1242
  if (missingDeps.length > 0) {
@@ -849,15 +1250,22 @@ async function installComponent(componentName, options = {}) {
849
1250
  }
850
1251
 
851
1252
  // ============================================================================
852
- // COMMANDS
1253
+ // COMMANDS - CLI Command Handlers
853
1254
  // ============================================================================
854
1255
 
1256
+ /**
1257
+ * Lists all available components from the registry
1258
+ * Shows component names and their dependencies
1259
+ *
1260
+ * @param {string} registryUrl - Registry URL or path
1261
+ */
855
1262
  async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
856
1263
  try {
857
1264
  const index = await loadIndex(registryUrl);
858
1265
 
859
1266
  console.log('\nAvailable components:\n');
860
1267
  index.registry.forEach(comp => {
1268
+ // Show dependencies if component has any
861
1269
  const deps = comp.registryDependencies && comp.registryDependencies.length > 0
862
1270
  ? ` (depends on: ${comp.registryDependencies.join(', ')})`
863
1271
  : '';
@@ -871,24 +1279,39 @@ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
871
1279
  }
872
1280
 
873
1281
  // ============================================================================
874
- // MAIN
1282
+ // MAIN - CLI Entry Point
875
1283
  // ============================================================================
876
1284
 
1285
+ /**
1286
+ * Main CLI entry point
1287
+ *
1288
+ * Parses command line arguments and routes to appropriate command handler:
1289
+ * - add: Install a component
1290
+ * - list: List available components
1291
+ * - css: Install/update CSS styles
1292
+ *
1293
+ * Supports options:
1294
+ * - --registry: Custom registry URL or path
1295
+ * - --overwrite / -f: Overwrite existing files
1296
+ */
877
1297
  async function main() {
878
1298
  const args = process.argv.slice(2);
879
1299
  const command = args[0];
880
1300
  const componentName = args[1];
881
1301
 
1302
+ // Parse --registry option if provided
882
1303
  const registryIndex = args.indexOf('--registry');
883
1304
  const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
884
1305
  ? args[registryIndex + 1]
885
1306
  : DEFAULT_REGISTRY_URL;
886
1307
 
1308
+ // Build options object
887
1309
  const options = {
888
1310
  overwrite: args.includes('--overwrite') || args.includes('-f'),
889
1311
  registryUrl: registryUrl,
890
1312
  };
891
1313
 
1314
+ // Show usage if no command provided
892
1315
  if (!command) {
893
1316
  console.log(`
894
1317
  Usage:
@@ -910,6 +1333,7 @@ Examples:
910
1333
  process.exit(0);
911
1334
  }
912
1335
 
1336
+ // Route to appropriate command handler
913
1337
  switch (command) {
914
1338
  case 'add':
915
1339
  if (!componentName) {
@@ -926,6 +1350,7 @@ Examples:
926
1350
 
927
1351
  case 'css':
928
1352
  case 'styles':
1353
+ // Both 'css' and 'styles' commands do the same thing
929
1354
  const config = loadComponentsConfig();
930
1355
  await installCSSStyles(config, registryUrl, options.overwrite, false);
931
1356
  break;
@@ -937,6 +1362,7 @@ Examples:
937
1362
  }
938
1363
  }
939
1364
 
1365
+ // Run main function and handle errors
940
1366
  main().catch(err => {
941
1367
  error(`Error: ${err.message}`);
942
1368
  process.exit(1);