weloop-kosign 1.0.9 → 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 +4 -1
  2. package/scripts/cli-remote.js +510 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weloop-kosign",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "CLI tool for installing Weloop UI components",
5
5
  "keywords": [
6
6
  "weloop",
@@ -26,5 +26,8 @@
26
26
  ],
27
27
  "engines": {
28
28
  "node": ">=18.0.0"
29
+ },
30
+ "scripts": {
31
+ "postpublish": "node scripts/postpublish.js"
29
32
  }
30
33
  }
@@ -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,113 @@ 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
+ */
674
+ function removeDuplicateCustomVariant(cssContent) {
675
+ const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
676
+ const matches = cssContent.match(variantPattern);
677
+
678
+ if (matches && matches.length > 1) {
679
+ // Remove all occurrences and add back one at the top
680
+ const withoutVariants = cssContent.replace(variantPattern, '');
681
+ return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
682
+ }
683
+
684
+ return cssContent;
685
+ }
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
+ */
475
768
  function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
476
769
  const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
477
770
  let processed = cssContent;
478
771
 
479
772
  if (!hasTwAnimate) {
773
+ // Package not installed - remove import to prevent build errors
480
774
  processed = cssContent.replace(twAnimatePattern, '');
481
775
  if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
482
776
  if (forceUpdate) {
@@ -490,10 +784,10 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
490
784
  }
491
785
  }
492
786
  } else {
493
- // Remove any duplicates first
787
+ // Package is installed - ensure import exists and remove duplicates
494
788
  processed = removeDuplicateTwAnimateImports(processed);
495
789
 
496
- // 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)
497
791
  if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
498
792
  if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
499
793
  processed = processed.replace(
@@ -509,10 +803,25 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
509
803
  return processed;
510
804
  }
511
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
+ */
512
813
  function removeTailwindImport(cssContent) {
513
814
  return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
514
815
  }
515
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
+ */
516
825
  function ensureTwAnimateImport(cssContent, hasTwAnimate) {
517
826
  if (!hasTwAnimate) {
518
827
  return cssContent;
@@ -526,7 +835,7 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
526
835
  return cleaned;
527
836
  }
528
837
 
529
- // Add import after tailwindcss if it exists
838
+ // Add import after tailwindcss if it exists, otherwise at the beginning
530
839
  if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
531
840
  return cleaned.replace(
532
841
  /(@import\s+["']tailwindcss["'];?\s*\n?)/,
@@ -537,57 +846,113 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
537
846
  return '@import "tw-animate-css";\n' + cleaned;
538
847
  }
539
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
+ */
540
862
  function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
541
863
  const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
542
864
  if (!tailwindMatch) {
865
+ // No tailwindcss import found - just prepend Weloop styles
543
866
  return weloopStyles + '\n\n' + existing;
544
867
  }
545
868
 
869
+ // Split existing CSS around the tailwindcss import
546
870
  const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
547
871
  const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
548
872
 
549
- // Check if tw-animate-css import already exists in existing or weloopStyles
873
+ // Check if tw-animate-css import already exists
550
874
  const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
551
875
  const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
552
876
 
877
+ // If existing file already has tw-animate import, remove it from Weloop styles
878
+ // to avoid duplicating it in the merge output
879
+ let weloopStylesCleaned = weloopStyles;
880
+ if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
881
+ weloopStylesCleaned = weloopStyles.replace(
882
+ /@import\s+["']tw-animate-css["'];?\s*\n?/g,
883
+ ''
884
+ );
885
+ }
886
+
887
+ // Add tw-animate import if needed (package installed but import missing)
553
888
  let importsToAdd = '';
554
889
  if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
555
890
  importsToAdd = '@import "tw-animate-css";\n';
556
891
  }
557
-
558
- return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStyles + '\n' + afterTailwind;
892
+
893
+ // Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
894
+ return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
559
895
  }
560
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
+ */
561
909
  function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
562
910
  const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
563
911
  const imports = existing.match(importPattern) || [];
564
912
  const importsText = imports.join('');
565
913
 
914
+ // Find where Weloop styles start (look for @theme inline or :root)
566
915
  const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
567
916
  const weloopStartMatch = existing.search(weloopStartPattern);
568
917
 
569
918
  if (weloopStartMatch === -1) {
919
+ // No existing Weloop styles found - just append
570
920
  return existing + '\n\n' + newStyles;
571
921
  }
572
922
 
923
+ // Extract content before Weloop styles
573
924
  let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
925
+ // Keep non-Weloop imports (filter out tw-animate if package not installed)
574
926
  const nonWeloopImports = importsText.split('\n').filter(imp =>
575
927
  !imp.includes('tw-animate-css') || hasTwAnimate
576
928
  ).join('\n');
929
+ // Remove all imports from contentBeforeWeloop (we'll add them back)
577
930
  contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
578
931
 
932
+ // Return: preserved imports + content before Weloop + new Weloop styles
579
933
  return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
580
934
  }
581
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
+ */
582
945
  async function fetchCSSFromRegistry(registryUrl) {
583
946
  let sourceCssPath;
584
947
 
585
948
  if (isLocalPath(registryUrl)) {
949
+ // Local path: find app/globals.css relative to registry directory
586
950
  const basePath = path.isAbsolute(registryUrl)
587
951
  ? path.dirname(registryUrl)
588
952
  : path.join(process.cwd(), path.dirname(registryUrl));
589
953
  sourceCssPath = path.join(basePath, 'app', 'globals.css');
590
954
  } else {
955
+ // Remote URL: replace /registry with /app/globals.css
591
956
  const baseUrl = registryUrl.replace('/registry', '');
592
957
  sourceCssPath = `${baseUrl}/app/globals.css`;
593
958
  }
@@ -595,17 +960,31 @@ async function fetchCSSFromRegistry(registryUrl) {
595
960
  return await fetchText(sourceCssPath);
596
961
  }
597
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
+ */
598
976
  async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
599
977
  const cssPath = config.tailwind?.css || 'app/globals.css';
600
978
  const fullCssPath = path.join(process.cwd(), cssPath);
601
979
 
602
- // Check if Weloop styles already exist
980
+ // Check if Weloop styles already exist in the file
603
981
  let hasWeloopStylesInFile = false;
604
982
  if (fs.existsSync(fullCssPath)) {
605
983
  const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
606
984
  hasWeloopStylesInFile = hasWeloopStyles(existingContent);
607
985
 
608
- // 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
609
988
  if (hasWeloopStylesInFile && !forceUpdate && silent) {
610
989
  return;
611
990
  }
@@ -622,10 +1001,13 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
622
1001
  const hasTwAnimate = checkPackageInstalled('tw-animate-css');
623
1002
  let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
624
1003
 
625
- // Handle file installation
1004
+ // Handle file installation based on mode and existing file state
626
1005
  if (forceUpdate && fs.existsSync(fullCssPath)) {
627
- // --overwrite: Replace entire file
628
- 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);
629
1011
  if (!silent) {
630
1012
  success(`Overwritten ${cssPath} with Weloop styles`);
631
1013
  if (hasTwAnimate) {
@@ -633,30 +1015,36 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
633
1015
  }
634
1016
  }
635
1017
  } else if (fs.existsSync(fullCssPath)) {
636
- // Normal mode: Merge intelligently
1018
+ // Mode 2: Normal update - Intelligently merge with existing styles
637
1019
  const existing = fs.readFileSync(fullCssPath, 'utf-8');
638
1020
  const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
639
1021
  existing.includes('@tailwind base');
640
1022
 
641
1023
  if (hasWeloopStylesInFile) {
642
- // Replace existing Weloop styles (only if different)
1024
+ // Case 2a: Weloop styles already exist - replace them with updated versions
643
1025
  let weloopStyles = removeTailwindImport(processedCssContent);
644
1026
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
645
- const finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
1027
+ let finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
1028
+ finalContent = removeDuplicateTwAnimateImports(finalContent);
1029
+ finalContent = removeDuplicateCustomVariant(finalContent);
1030
+ finalContent = normalizeCSSFormat(finalContent);
646
1031
 
647
- // Only update if content actually changed
1032
+ // Only write if content actually changed (prevents unnecessary file updates)
648
1033
  if (finalContent !== existing) {
649
1034
  fs.writeFileSync(fullCssPath, finalContent);
650
1035
  if (!silent) {
651
1036
  success(`Updated ${cssPath} with Weloop styles`);
652
1037
  }
653
1038
  }
654
- // If no changes, silently skip
1039
+ // If no changes, silently skip (file is already up to date)
655
1040
  } else if (hasTailwindImport) {
656
- // Merge after Tailwind imports
1041
+ // Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
657
1042
  let weloopStyles = removeTailwindImport(processedCssContent);
658
1043
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
659
- const merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
1044
+ let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
1045
+ merged = removeDuplicateTwAnimateImports(merged);
1046
+ merged = removeDuplicateCustomVariant(merged);
1047
+ merged = normalizeCSSFormat(merged);
660
1048
  fs.writeFileSync(fullCssPath, merged);
661
1049
  if (!silent) {
662
1050
  success(`Updated ${cssPath} with Weloop styles`);
@@ -666,9 +1054,10 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
666
1054
  info(` Your existing styles are preserved`);
667
1055
  }
668
1056
  } else {
669
- // No Tailwind imports, prepend everything
1057
+ // Case 2c: No Tailwind imports - prepend Weloop styles to existing content
670
1058
  let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
671
1059
 
1060
+ // Ensure tw-animate import exists if package is installed
672
1061
  if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
673
1062
  if (finalCssContent.includes('@import "tailwindcss"')) {
674
1063
  finalCssContent = finalCssContent.replace(
@@ -679,7 +1068,9 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
679
1068
  finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
680
1069
  }
681
1070
  }
682
- fs.writeFileSync(fullCssPath, finalCssContent + '\n\n' + existing);
1071
+ let combined = finalCssContent + '\n\n' + existing;
1072
+ combined = normalizeCSSFormat(combined);
1073
+ fs.writeFileSync(fullCssPath, combined);
683
1074
  if (!silent) {
684
1075
  success(`Updated ${cssPath} with Weloop styles`);
685
1076
  if (hasTwAnimate) {
@@ -688,8 +1079,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
688
1079
  }
689
1080
  }
690
1081
  } else {
691
- // Create new file
692
- 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);
693
1087
  if (!silent) {
694
1088
  success(`Created ${cssPath} with Weloop styles`);
695
1089
  if (hasTwAnimate) {
@@ -709,13 +1103,22 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
709
1103
  }
710
1104
 
711
1105
  // ============================================================================
712
- // COMPONENT INSTALLATION
1106
+ // COMPONENT INSTALLATION - Installing Components from Registry
713
1107
  // ============================================================================
714
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
+ */
715
1117
  function createUtilsFile(utilsPath) {
716
1118
  if (fs.existsSync(utilsPath)) return;
717
1119
 
718
1120
  // Silently create utils file (matching shadcn/ui behavior)
1121
+ // Don't overwrite if user has customized it
719
1122
  ensureDirectoryExists(path.dirname(utilsPath));
720
1123
 
721
1124
  const utilsContent = `import { clsx, type ClassValue } from "clsx"
@@ -728,10 +1131,27 @@ export function cn(...inputs: ClassValue[]) {
728
1131
  fs.writeFileSync(utilsPath, utilsContent);
729
1132
  }
730
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
+ */
731
1151
  async function installComponent(componentName, options = {}) {
732
1152
  const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
733
1153
 
734
- // Install base dependencies first (required for utils.ts)
1154
+ // Step 1: Install base dependencies (required for utils.ts to work)
735
1155
  const baseDeps = [];
736
1156
  if (!checkPackageInstalled('clsx')) {
737
1157
  baseDeps.push('clsx');
@@ -744,19 +1164,20 @@ async function installComponent(componentName, options = {}) {
744
1164
  await installPackages(baseDeps);
745
1165
  }
746
1166
 
747
- // Load or create components.json (after base deps are installed)
1167
+ // Step 2: Load or create components.json configuration
748
1168
  const config = loadComponentsConfig();
749
1169
 
750
- // Install tw-animate-css automatically (required for animations)
1170
+ // Step 3: Install tw-animate-css automatically (required for component animations)
751
1171
  if (!checkPackageInstalled('tw-animate-css')) {
752
1172
  info('Installing tw-animate-css for animations...');
753
1173
  await installPackages(['tw-animate-css']);
754
1174
  }
755
1175
 
756
- // 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
757
1178
  await installCSSStyles(config, registryUrl, false, true);
758
1179
 
759
- // Get paths from components.json
1180
+ // Step 5: Resolve paths from components.json configuration
760
1181
  const uiAlias = config.aliases?.ui || '@/components/ui';
761
1182
  const utilsAlias = config.aliases?.utils || '@/lib/utils';
762
1183
 
@@ -767,30 +1188,34 @@ async function installComponent(componentName, options = {}) {
767
1188
  }
768
1189
  utilsPath = path.join(process.cwd(), utilsPath);
769
1190
 
770
- // Load component registry
1191
+ // Step 6: Load component registry from remote or local source
771
1192
  const registry = await loadRegistry(componentName, registryUrl);
772
1193
 
773
1194
  if (!registry) {
774
1195
  error(`Component "${componentName}" not found in registry.`);
775
1196
  info('Available components:');
776
1197
  try {
1198
+ // Show available components to help user
777
1199
  const index = await loadIndex(registryUrl);
778
1200
  index.registry.forEach(comp => {
779
1201
  console.log(` - ${comp.name}`);
780
1202
  });
781
1203
  } catch (e) {
782
- // Ignore if index fails
1204
+ // Ignore if index fails - we already showed the error
783
1205
  }
784
1206
  process.exit(1);
785
1207
  }
786
1208
 
787
- // 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
788
1212
  const componentPath = path.join(componentsDir, `${componentName}.tsx`);
789
1213
  if (fs.existsSync(componentPath) && !overwrite) {
790
1214
  return;
791
1215
  }
792
1216
 
793
- // 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)
794
1219
  if (registry.registryDependencies && registry.registryDependencies.length > 0) {
795
1220
  info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
796
1221
  for (const dep of registry.registryDependencies) {
@@ -798,7 +1223,8 @@ async function installComponent(componentName, options = {}) {
798
1223
  }
799
1224
  }
800
1225
 
801
- // Install component files
1226
+ // Step 9: Install component files
1227
+ // Components can have multiple files (e.g., component + types + styles)
802
1228
  for (const file of registry.files) {
803
1229
  const filePath = path.join(process.cwd(), file.path);
804
1230
  ensureDirectoryExists(path.dirname(filePath));
@@ -806,10 +1232,11 @@ async function installComponent(componentName, options = {}) {
806
1232
  success(`Installed: ${file.path}`);
807
1233
  }
808
1234
 
809
- // 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
810
1237
  createUtilsFile(utilsPath);
811
1238
 
812
- // Install npm dependencies
1239
+ // Step 11: Install npm dependencies required by the component
813
1240
  if (registry.dependencies && registry.dependencies.length > 0) {
814
1241
  const missingDeps = getMissingDependencies(registry.dependencies);
815
1242
  if (missingDeps.length > 0) {
@@ -823,15 +1250,22 @@ async function installComponent(componentName, options = {}) {
823
1250
  }
824
1251
 
825
1252
  // ============================================================================
826
- // COMMANDS
1253
+ // COMMANDS - CLI Command Handlers
827
1254
  // ============================================================================
828
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
+ */
829
1262
  async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
830
1263
  try {
831
1264
  const index = await loadIndex(registryUrl);
832
1265
 
833
1266
  console.log('\nAvailable components:\n');
834
1267
  index.registry.forEach(comp => {
1268
+ // Show dependencies if component has any
835
1269
  const deps = comp.registryDependencies && comp.registryDependencies.length > 0
836
1270
  ? ` (depends on: ${comp.registryDependencies.join(', ')})`
837
1271
  : '';
@@ -845,24 +1279,39 @@ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
845
1279
  }
846
1280
 
847
1281
  // ============================================================================
848
- // MAIN
1282
+ // MAIN - CLI Entry Point
849
1283
  // ============================================================================
850
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
+ */
851
1297
  async function main() {
852
1298
  const args = process.argv.slice(2);
853
1299
  const command = args[0];
854
1300
  const componentName = args[1];
855
1301
 
1302
+ // Parse --registry option if provided
856
1303
  const registryIndex = args.indexOf('--registry');
857
1304
  const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
858
1305
  ? args[registryIndex + 1]
859
1306
  : DEFAULT_REGISTRY_URL;
860
1307
 
1308
+ // Build options object
861
1309
  const options = {
862
1310
  overwrite: args.includes('--overwrite') || args.includes('-f'),
863
1311
  registryUrl: registryUrl,
864
1312
  };
865
1313
 
1314
+ // Show usage if no command provided
866
1315
  if (!command) {
867
1316
  console.log(`
868
1317
  Usage:
@@ -884,6 +1333,7 @@ Examples:
884
1333
  process.exit(0);
885
1334
  }
886
1335
 
1336
+ // Route to appropriate command handler
887
1337
  switch (command) {
888
1338
  case 'add':
889
1339
  if (!componentName) {
@@ -900,6 +1350,7 @@ Examples:
900
1350
 
901
1351
  case 'css':
902
1352
  case 'styles':
1353
+ // Both 'css' and 'styles' commands do the same thing
903
1354
  const config = loadComponentsConfig();
904
1355
  await installCSSStyles(config, registryUrl, options.overwrite, false);
905
1356
  break;
@@ -911,6 +1362,7 @@ Examples:
911
1362
  }
912
1363
  }
913
1364
 
1365
+ // Run main function and handle errors
914
1366
  main().catch(err => {
915
1367
  error(`Error: ${err.message}`);
916
1368
  process.exit(1);