weloop-kosign 1.1.2 → 1.1.4

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.
@@ -48,22 +48,81 @@ const { execSync } = require('child_process');
48
48
  */
49
49
  function getDefaultRegistryUrl() {
50
50
  const localRegistryPath = path.join(__dirname, '../registry');
51
-
51
+
52
52
  // Check if we're running in the component library project itself
53
53
  if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
54
54
  return localRegistryPath;
55
55
  }
56
-
56
+
57
57
  // Fall back to environment variable or default remote URL
58
- return process.env.WELOOP_REGISTRY_URL ||
58
+ return process.env.WELOOP_REGISTRY_URL ||
59
59
  'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
60
60
  }
61
61
 
62
62
  const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
63
63
 
64
+ // ============================================================================
65
+ // CONSTANTS
66
+ // ============================================================================
67
+
64
68
  // Network timeout constants (in milliseconds)
65
69
  const REQUEST_TIMEOUT = 15000;
66
70
  const RETRY_DELAY = 1000;
71
+ const MAX_RETRIES = 3;
72
+
73
+ // Package names
74
+ const PACKAGE_NAMES = {
75
+ CLSX: 'clsx',
76
+ TAILWIND_MERGE: 'tailwind-merge',
77
+ TW_ANIMATE_CSS: 'tw-animate-css',
78
+ };
79
+
80
+ // Base dependencies required for all components
81
+ const BASE_DEPENDENCIES = [PACKAGE_NAMES.CLSX, PACKAGE_NAMES.TAILWIND_MERGE];
82
+
83
+ // File paths
84
+ const PATHS = {
85
+ COMPONENTS_JSON: 'components.json',
86
+ REGISTRY_INDEX: 'index.json',
87
+ APP_GLOBALS_CSS: 'app/globals.css',
88
+ STYLES_GLOBALS_CSS: 'styles/globals.css',
89
+ VITE_INDEX_CSS: 'src/index.css',
90
+ PACKAGE_JSON: 'package.json',
91
+ };
92
+
93
+ // Default configuration values
94
+ const DEFAULTS = {
95
+ STYLE: 'blue',
96
+ BASE_COLOR: 'neutral',
97
+ CSS_PATH: PATHS.APP_GLOBALS_CSS,
98
+ SCHEMA_URL: 'https://weloop.kosign.dev/schema.json',
99
+ ICON_LIBRARY: 'lucide',
100
+ };
101
+
102
+ // Registry URLs
103
+ const REGISTRY_URLS = {
104
+ DEFAULT: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry',
105
+ INDEX: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry/index.json',
106
+ CSS: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/app/globals.css',
107
+ };
108
+
109
+ // CSS patterns for regex matching
110
+ const CSS_PATTERNS = {
111
+ TAILWIND_IMPORT: /@import\s+["']tailwindcss["'];?\s*\n?/g,
112
+ TW_ANIMATE_IMPORT: /@import\s+["']tw-animate-css["'];?\s*\n?/g,
113
+ CUSTOM_VARIANT: /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g,
114
+ IMPORT_STATEMENT: /@import\s+["'][^"']+["'];?\s*\n?/g,
115
+ WELOOP_START: /(@theme\s+inline|:root\s*\{)/,
116
+ };
117
+
118
+ // Weloop style markers
119
+ const WELOOP_MARKERS = [
120
+ '--WLDS-PRM-',
121
+ '--WLDS-RED-',
122
+ '--WLDS-NTL-',
123
+ '--system-100',
124
+ '--system-200',
125
+ ];
67
126
 
68
127
  // ============================================================================
69
128
  // UTILITIES - Logging and File System Helpers
@@ -130,6 +189,38 @@ function ensureDirectoryExists(dirPath) {
130
189
  }
131
190
  }
132
191
 
192
+ /**
193
+ * Normalizes and cleans CSS content by removing duplicates and ensuring proper format
194
+ * @param {string} cssContent - CSS content to normalize
195
+ * @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
196
+ * @returns {string} Normalized CSS content
197
+ */
198
+ function normalizeAndCleanCSS(cssContent, hasTwAnimate) {
199
+ let normalized = normalizeCSSFormat(cssContent);
200
+ normalized = removeDuplicateTwAnimateImports(normalized);
201
+ normalized = removeDuplicateCustomVariant(normalized);
202
+ // Ensure tw-animate import is handled correctly
203
+ normalized = processTwAnimateImport(normalized, hasTwAnimate, false);
204
+ return normalized;
205
+ }
206
+
207
+ /**
208
+ * Creates error message for CSS installation failures with helpful guidance
209
+ * @param {string} registryUrl - Registry URL that was attempted
210
+ * @param {string} cssPath - Target CSS file path
211
+ * @returns {string} Formatted error message with instructions
212
+ */
213
+ function getCSSInstallErrorMessage(registryUrl, cssPath) {
214
+ const cssUrl = isLocalPath(registryUrl)
215
+ ? 'your local registry/app/globals.css'
216
+ : registryUrl.replace('/registry', '/app/globals.css');
217
+
218
+ return `\n To add styles manually:\n` +
219
+ ` 1. Download: ${cssUrl}\n` +
220
+ ` 2. Add the CSS variables to your ${cssPath} file\n` +
221
+ ` 3. Or copy from: ${REGISTRY_URLS.CSS}\n`;
222
+ }
223
+
133
224
  /**
134
225
  * Checks if a path is a local file path or a remote URL
135
226
  * @param {string} pathOrUrl - Path or URL to check
@@ -158,12 +249,12 @@ function detectPackageManager() {
158
249
  if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
159
250
  if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
160
251
  if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
161
-
252
+
162
253
  // Fallback: check npm user agent (set by npm/yarn/pnpm when running)
163
254
  const userAgent = process.env.npm_config_user_agent || '';
164
255
  if (userAgent.includes('yarn')) return 'yarn';
165
256
  if (userAgent.includes('pnpm')) return 'pnpm';
166
-
257
+
167
258
  // Default to npm if we can't detect anything
168
259
  return 'npm';
169
260
  }
@@ -191,9 +282,9 @@ function getInstallCommand(packageManager, packages) {
191
282
  * @returns {boolean} True if package is installed, false otherwise
192
283
  */
193
284
  function checkPackageInstalled(packageName) {
194
- const packageJsonPath = path.join(process.cwd(), 'package.json');
285
+ const packageJsonPath = path.join(process.cwd(), PATHS.PACKAGE_JSON);
195
286
  if (!fs.existsSync(packageJsonPath)) return false;
196
-
287
+
197
288
  try {
198
289
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
199
290
  // Check both dependencies and devDependencies
@@ -212,16 +303,16 @@ function checkPackageInstalled(packageName) {
212
303
  * @returns {string[]} List of packages that need to be installed
213
304
  */
214
305
  function getMissingDependencies(requiredDeps) {
215
- const packageJsonPath = path.join(process.cwd(), 'package.json');
306
+ const packageJsonPath = path.join(process.cwd(), PATHS.PACKAGE_JSON);
216
307
  if (!fs.existsSync(packageJsonPath)) {
217
308
  // No package.json means we need to install everything
218
309
  return requiredDeps;
219
310
  }
220
-
311
+
221
312
  try {
222
313
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
223
314
  const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
224
-
315
+
225
316
  return requiredDeps.filter(dep => {
226
317
  // For scoped packages like @radix-ui/react-button, also check @radix-ui
227
318
  const depName = dep.split('/').slice(0, 2).join('/');
@@ -243,44 +334,44 @@ function getMissingDependencies(requiredDeps) {
243
334
  */
244
335
  async function installPackages(packages) {
245
336
  if (packages.length === 0) return;
246
-
337
+
247
338
  const packageManager = detectPackageManager();
248
339
  info(`\nInstalling dependencies: ${packages.join(', ')}`);
249
-
340
+
250
341
  try {
251
342
  const installCmd = getInstallCommand(packageManager, packages);
252
-
343
+
253
344
  // npm has noisy ERESOLVE warnings that aren't real errors
254
345
  // We filter these out while keeping actual error messages
255
346
  if (packageManager === 'npm') {
256
347
  const { spawn } = require('child_process');
257
348
  const [cmd, ...args] = installCmd.split(' ');
258
-
349
+
259
350
  await new Promise((resolve, reject) => {
260
351
  const proc = spawn(cmd, args, {
261
352
  cwd: process.cwd(),
262
353
  stdio: ['inherit', 'inherit', 'pipe'],
263
354
  shell: true
264
355
  });
265
-
356
+
266
357
  let stderr = '';
267
358
  proc.stderr.on('data', (data) => {
268
359
  const output = data.toString();
269
360
  // Filter out ERESOLVE peer dependency warnings (these are usually safe to ignore)
270
361
  // but keep actual error messages visible to the user
271
- if (!output.includes('ERESOLVE overriding peer dependency') &&
272
- !output.includes('npm warn ERESOLVE')) {
362
+ if (!output.includes('ERESOLVE overriding peer dependency') &&
363
+ !output.includes('npm warn ERESOLVE')) {
273
364
  process.stderr.write(data);
274
365
  }
275
366
  stderr += output;
276
367
  });
277
-
368
+
278
369
  proc.on('close', (code) => {
279
370
  if (code !== 0) {
280
371
  // npm sometimes exits with non-zero code due to warnings, not real errors
281
372
  // Check if there's an actual error message (not just ERESOLVE warnings)
282
- const hasRealError = stderr.includes('npm error') &&
283
- !stderr.match(/npm error.*ERESOLVE/);
373
+ const hasRealError = stderr.includes('npm error') &&
374
+ !stderr.match(/npm error.*ERESOLVE/);
284
375
  if (hasRealError) {
285
376
  reject(new Error(`Installation failed with code ${code}`));
286
377
  } else {
@@ -291,13 +382,13 @@ async function installPackages(packages) {
291
382
  resolve();
292
383
  }
293
384
  });
294
-
385
+
295
386
  proc.on('error', reject);
296
387
  });
297
388
  } else {
298
389
  execSync(installCmd, { stdio: 'inherit', cwd: process.cwd() });
299
390
  }
300
-
391
+
301
392
  success(`Dependencies installed successfully`);
302
393
  } catch (error) {
303
394
  warn(`Failed to install dependencies automatically`);
@@ -319,25 +410,25 @@ async function installPackages(packages) {
319
410
  * @returns {object} The default configuration object
320
411
  */
321
412
  function createDefaultComponentsConfig() {
322
- const configPath = path.join(process.cwd(), 'components.json');
323
-
413
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
414
+
324
415
  // Detect Next.js project structure
325
416
  // App Router uses 'app/globals.css', Pages Router uses 'styles/globals.css'
326
417
  const hasAppDir = fs.existsSync(path.join(process.cwd(), 'app'));
327
- const cssPath = hasAppDir ? 'app/globals.css' : 'styles/globals.css';
328
-
418
+ const cssPath = hasAppDir ? PATHS.APP_GLOBALS_CSS : PATHS.STYLES_GLOBALS_CSS;
419
+
329
420
  const defaultConfig = {
330
- style: 'blue',
421
+ style: DEFAULTS.STYLE,
331
422
  rsc: true,
332
423
  tsx: true,
333
424
  tailwind: {
334
425
  config: 'tailwind.config.ts',
335
426
  css: cssPath,
336
- baseColor: 'neutral',
427
+ baseColor: DEFAULTS.BASE_COLOR,
337
428
  cssVariables: true,
338
429
  prefix: ''
339
430
  },
340
- iconLibrary: 'lucide',
431
+ iconLibrary: DEFAULTS.ICON_LIBRARY,
341
432
  aliases: {
342
433
  components: '@/components',
343
434
  utils: '@/lib/utils',
@@ -345,13 +436,13 @@ function createDefaultComponentsConfig() {
345
436
  lib: '@/lib',
346
437
  hooks: '@/hooks'
347
438
  },
348
- registry: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry/index.json'
439
+ registry: REGISTRY_URLS.INDEX
349
440
  };
350
-
441
+
351
442
  fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
352
- success(`Created components.json`);
443
+ success(`Created ${PATHS.COMPONENTS_JSON}`);
353
444
  info(` You can customize this file to match your project setup`);
354
-
445
+
355
446
  return defaultConfig;
356
447
  }
357
448
 
@@ -362,15 +453,20 @@ function createDefaultComponentsConfig() {
362
453
  * @returns {object} The configuration object
363
454
  */
364
455
  function loadComponentsConfig() {
365
- const configPath = path.join(process.cwd(), 'components.json');
366
-
456
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
457
+
367
458
  if (!fs.existsSync(configPath)) {
368
- info('components.json not found. Creating default configuration...');
459
+ info(`${PATHS.COMPONENTS_JSON} not found. Creating default configuration...`);
369
460
  return createDefaultComponentsConfig();
370
461
  }
371
-
372
- const content = fs.readFileSync(configPath, 'utf-8');
373
- return JSON.parse(content);
462
+
463
+ try {
464
+ const content = fs.readFileSync(configPath, 'utf-8');
465
+ return JSON.parse(content);
466
+ } catch (err) {
467
+ error(`Failed to parse ${PATHS.COMPONENTS_JSON}: ${err.message}`);
468
+ throw err;
469
+ }
374
470
  }
375
471
 
376
472
  /**
@@ -386,21 +482,21 @@ function loadComponentsConfig() {
386
482
  * @param {number} retries - Number of retry attempts (default: 3)
387
483
  * @returns {Promise<object>} Parsed JSON object
388
484
  */
389
- async function fetchJSON(urlOrPath, retries = 3) {
485
+ async function fetchJSON(urlOrPath, retries = MAX_RETRIES) {
390
486
  // Handle local file paths
391
487
  if (isLocalPath(urlOrPath)) {
392
488
  return new Promise((resolve, reject) => {
393
489
  try {
394
490
  // Resolve relative paths to absolute
395
- const fullPath = path.isAbsolute(urlOrPath)
396
- ? urlOrPath
491
+ const fullPath = path.isAbsolute(urlOrPath)
492
+ ? urlOrPath
397
493
  : path.join(process.cwd(), urlOrPath);
398
-
494
+
399
495
  if (!fs.existsSync(fullPath)) {
400
496
  reject(new Error(`File not found: ${fullPath}`));
401
497
  return;
402
498
  }
403
-
499
+
404
500
  const content = fs.readFileSync(fullPath, 'utf-8');
405
501
  resolve(JSON.parse(content));
406
502
  } catch (e) {
@@ -408,38 +504,38 @@ async function fetchJSON(urlOrPath, retries = 3) {
408
504
  }
409
505
  });
410
506
  }
411
-
507
+
412
508
  // Handle remote URLs
413
509
  return new Promise((resolve, reject) => {
414
510
  const client = urlOrPath.startsWith('https') ? https : http;
415
511
  let timeout;
416
512
  let request;
417
-
513
+
418
514
  const makeRequest = () => {
419
515
  request = client.get(urlOrPath, (res) => {
420
516
  clearTimeout(timeout);
421
-
517
+
422
518
  // Handle HTTP redirects (301, 302)
423
519
  if (res.statusCode === 302 || res.statusCode === 301) {
424
520
  return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
425
521
  }
426
-
522
+
427
523
  // Provide helpful error messages for common HTTP status codes
428
524
  if (res.statusCode === 403) {
429
525
  reject(new Error(`Access forbidden (403). Repository may be private. Make it public in GitLab/GitHub settings, or use --registry with a public URL.`));
430
526
  return;
431
527
  }
432
-
528
+
433
529
  if (res.statusCode === 404) {
434
530
  reject(new Error(`Not found (404). Check that the registry files are pushed to the repository and the URL is correct.`));
435
531
  return;
436
532
  }
437
-
533
+
438
534
  if (res.statusCode !== 200) {
439
535
  reject(new Error(`Failed to fetch: HTTP ${res.statusCode} - ${res.statusMessage || 'Unknown error'}`));
440
536
  return;
441
537
  }
442
-
538
+
443
539
  // Collect response data
444
540
  let data = '';
445
541
  res.on('data', (chunk) => { data += chunk; });
@@ -456,7 +552,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
456
552
  }
457
553
  });
458
554
  });
459
-
555
+
460
556
  // Handle network errors with automatic retry
461
557
  request.on('error', (err) => {
462
558
  clearTimeout(timeout);
@@ -469,7 +565,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
469
565
  reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
470
566
  }
471
567
  });
472
-
568
+
473
569
  // Set request timeout with retry logic
474
570
  timeout = setTimeout(() => {
475
571
  request.destroy();
@@ -483,7 +579,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
483
579
  }
484
580
  }, REQUEST_TIMEOUT);
485
581
  };
486
-
582
+
487
583
  makeRequest();
488
584
  });
489
585
  }
@@ -503,34 +599,34 @@ async function fetchText(urlOrPath) {
503
599
  }
504
600
  return fs.readFileSync(urlOrPath, 'utf-8');
505
601
  }
506
-
602
+
507
603
  // Handle remote URLs
508
604
  return new Promise((resolve, reject) => {
509
605
  const client = urlOrPath.startsWith('https') ? https : http;
510
606
  let data = '';
511
-
607
+
512
608
  const req = client.get(urlOrPath, (res) => {
513
609
  // Follow redirects
514
610
  if (res.statusCode === 302 || res.statusCode === 301) {
515
611
  return fetchText(res.headers.location).then(resolve).catch(reject);
516
612
  }
517
-
613
+
518
614
  // Handle common HTTP errors
519
615
  if (res.statusCode === 403) {
520
616
  reject(new Error('Access forbidden - repository may be private'));
521
617
  return;
522
618
  }
523
-
619
+
524
620
  if (res.statusCode === 404) {
525
621
  reject(new Error('CSS file not found in repository'));
526
622
  return;
527
623
  }
528
-
624
+
529
625
  if (res.statusCode !== 200) {
530
626
  reject(new Error(`HTTP ${res.statusCode}`));
531
627
  return;
532
628
  }
533
-
629
+
534
630
  // Collect response data
535
631
  res.on('data', (chunk) => { data += chunk; });
536
632
  res.on('end', () => {
@@ -542,7 +638,7 @@ async function fetchText(urlOrPath) {
542
638
  resolve(data);
543
639
  });
544
640
  });
545
-
641
+
546
642
  // Handle network errors with automatic retry
547
643
  req.on('error', (err) => {
548
644
  if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
@@ -553,7 +649,7 @@ async function fetchText(urlOrPath) {
553
649
  reject(err);
554
650
  }
555
651
  });
556
-
652
+
557
653
  // Set request timeout
558
654
  req.setTimeout(REQUEST_TIMEOUT, () => {
559
655
  req.destroy();
@@ -573,13 +669,14 @@ async function fetchText(urlOrPath) {
573
669
  function resolveRegistryPath(baseUrl, fileName) {
574
670
  if (isLocalPath(baseUrl)) {
575
671
  // Resolve local paths (absolute or relative)
576
- const basePath = path.isAbsolute(baseUrl)
577
- ? baseUrl
672
+ const basePath = path.isAbsolute(baseUrl)
673
+ ? baseUrl
578
674
  : path.join(process.cwd(), baseUrl);
579
675
  return path.join(basePath, fileName);
580
676
  }
581
- // For remote URLs, just append the filename
582
- return `${baseUrl}/${fileName}`;
677
+ // For remote URLs, append filename with proper separator
678
+ const separator = baseUrl.endsWith('/') ? '' : '/';
679
+ return `${baseUrl}${separator}${fileName}`;
583
680
  }
584
681
 
585
682
  /**
@@ -596,6 +693,7 @@ async function loadRegistry(componentName, registryBaseUrl) {
596
693
  return await fetchJSON(registryPath);
597
694
  } catch (err) {
598
695
  // Component not found - return null instead of throwing
696
+ // This allows the caller to handle missing components gracefully
599
697
  return null;
600
698
  }
601
699
  }
@@ -608,7 +706,7 @@ async function loadRegistry(componentName, registryBaseUrl) {
608
706
  * @returns {Promise<object>} Index object containing list of components
609
707
  */
610
708
  async function loadIndex(registryBaseUrl) {
611
- const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
709
+ const indexPath = resolveRegistryPath(registryBaseUrl, PATHS.REGISTRY_INDEX);
612
710
  try {
613
711
  return await fetchJSON(indexPath);
614
712
  } catch (err) {
@@ -629,12 +727,18 @@ async function loadIndex(registryBaseUrl) {
629
727
  * @returns {boolean} True if Weloop styles are present
630
728
  */
631
729
  function hasWeloopStyles(content) {
632
- // Check for Weloop-specific CSS variable prefixes
633
- return content.includes('--WLDS-PRM-') || // Primary colors
634
- content.includes('--WLDS-RED-') || // Red colors
635
- content.includes('--WLDS-NTL-') || // Neutral colors
636
- content.includes('--system-100') || // System colors
637
- content.includes('--system-200');
730
+ return WELOOP_MARKERS.some(marker => content.includes(marker));
731
+ }
732
+
733
+ /**
734
+ * Checks if CSS content has a Tailwind import (either format)
735
+ * @param {string} content - CSS content to check
736
+ * @returns {boolean} True if Tailwind import exists
737
+ */
738
+ function hasTailwindImport(content) {
739
+ return content.includes('@import "tailwindcss"') ||
740
+ content.includes("@import 'tailwindcss'") ||
741
+ content.includes('@tailwind base');
638
742
  }
639
743
 
640
744
  /**
@@ -645,16 +749,15 @@ function hasWeloopStyles(content) {
645
749
  * @returns {string} CSS content with duplicates removed
646
750
  */
647
751
  function removeDuplicateTwAnimateImports(cssContent) {
648
- const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
649
- const matches = cssContent.match(twAnimatePattern);
650
-
752
+ const matches = cssContent.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
753
+
651
754
  if (matches && matches.length > 1) {
652
755
  // Remove all occurrences first
653
- let cleaned = cssContent.replace(twAnimatePattern, '');
756
+ let cleaned = cssContent.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
654
757
  // Add it back once, right after tailwindcss import if it exists
655
- if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
758
+ if (hasTailwindImport(cleaned)) {
656
759
  cleaned = cleaned.replace(
657
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
760
+ CSS_PATTERNS.TAILWIND_IMPORT,
658
761
  '$1@import "tw-animate-css";\n'
659
762
  );
660
763
  } else {
@@ -663,7 +766,7 @@ function removeDuplicateTwAnimateImports(cssContent) {
663
766
  }
664
767
  return cleaned;
665
768
  }
666
-
769
+
667
770
  return cssContent;
668
771
  }
669
772
 
@@ -675,12 +778,11 @@ function removeDuplicateTwAnimateImports(cssContent) {
675
778
  * @returns {string} CSS content with duplicates removed
676
779
  */
677
780
  function removeDuplicateCustomVariant(cssContent) {
678
- const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
679
- const matches = cssContent.match(variantPattern);
781
+ const matches = cssContent.match(CSS_PATTERNS.CUSTOM_VARIANT);
680
782
 
681
783
  if (matches && matches.length > 1) {
682
784
  // Remove all occurrences and add back one at the top
683
- const withoutVariants = cssContent.replace(variantPattern, '');
785
+ const withoutVariants = cssContent.replace(CSS_PATTERNS.CUSTOM_VARIANT, '');
684
786
  return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
685
787
  }
686
788
 
@@ -703,30 +805,28 @@ function removeDuplicateCustomVariant(cssContent) {
703
805
  */
704
806
  function normalizeCSSFormat(cssContent) {
705
807
  // Extract @custom-variant declaration
706
- const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
707
- const variantMatch = cssContent.match(variantPattern);
808
+ const variantMatch = cssContent.match(CSS_PATTERNS.CUSTOM_VARIANT);
708
809
  const hasVariant = variantMatch && variantMatch.length > 0;
709
-
810
+
710
811
  // Extract all @import statements
711
- const importPattern = /@import\s+["'][^"']+["'];?\s*\n?/g;
712
- const imports = cssContent.match(importPattern) || [];
713
-
812
+ const imports = cssContent.match(CSS_PATTERNS.IMPORT_STATEMENT) || [];
813
+
714
814
  // Separate imports by type for proper ordering
715
815
  const tailwindImport = imports.find(imp => imp.includes('tailwindcss'));
716
816
  const twAnimateImport = imports.find(imp => imp.includes('tw-animate-css'));
717
- const otherImports = imports.filter(imp =>
817
+ const otherImports = imports.filter(imp =>
718
818
  !imp.includes('tailwindcss') && !imp.includes('tw-animate-css')
719
819
  );
720
-
820
+
721
821
  // Remove all imports and variant from content to get the rest
722
822
  let content = cssContent
723
- .replace(variantPattern, '')
724
- .replace(importPattern, '')
823
+ .replace(CSS_PATTERNS.CUSTOM_VARIANT, '')
824
+ .replace(CSS_PATTERNS.IMPORT_STATEMENT, '')
725
825
  .trim();
726
-
826
+
727
827
  // Build normalized format in the correct order
728
828
  let normalized = '';
729
-
829
+
730
830
  // Step 1: Imports first (tailwindcss, then tw-animate-css, then others)
731
831
  if (tailwindImport) {
732
832
  normalized += tailwindImport.trim() + '\n';
@@ -737,23 +837,19 @@ function normalizeCSSFormat(cssContent) {
737
837
  if (otherImports.length > 0) {
738
838
  normalized += otherImports.join('') + '\n';
739
839
  }
740
-
840
+
741
841
  // Step 2: @custom-variant second (if it exists)
742
842
  if (hasVariant) {
743
- if (normalized) {
744
- normalized += '\n@custom-variant dark (&:is(.dark *));\n';
745
- } else {
746
- normalized += '@custom-variant dark (&:is(.dark *));\n';
747
- }
843
+ normalized += normalized ? '\n@custom-variant dark (&:is(.dark *));\n' : '@custom-variant dark (&:is(.dark *));\n';
748
844
  }
749
-
845
+
750
846
  // Step 3: Rest of content (theme variables, etc.)
751
847
  if (normalized && content) {
752
848
  normalized += '\n' + content;
753
849
  } else if (content) {
754
850
  normalized = content;
755
851
  }
756
-
852
+
757
853
  return normalized.trim() + (normalized ? '\n' : '');
758
854
  }
759
855
 
@@ -769,32 +865,32 @@ function normalizeCSSFormat(cssContent) {
769
865
  * @returns {string} Processed CSS content
770
866
  */
771
867
  function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
772
- const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
773
868
  let processed = cssContent;
774
-
869
+
775
870
  if (!hasTwAnimate) {
776
871
  // Package not installed - remove import to prevent build errors
777
- processed = cssContent.replace(twAnimatePattern, '');
872
+ processed = cssContent.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
778
873
  if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
874
+ const installCmd = `npm install ${PACKAGE_NAMES.TW_ANIMATE_CSS}`;
779
875
  if (forceUpdate) {
780
- warn('tw-animate-css package not found - removed from CSS to prevent build errors');
781
- info(' Install with: npm install tw-animate-css');
782
- info(' Then run: node scripts/weloop-cli.js css --overwrite to include it');
876
+ warn(`${PACKAGE_NAMES.TW_ANIMATE_CSS} package not found - removed from CSS to prevent build errors`);
877
+ info(` Install with: ${installCmd}`);
878
+ info(' Then run: node scripts/cli-remote.js css --overwrite to include it');
783
879
  } else {
784
- warn('tw-animate-css package not found - removed from CSS');
785
- info(' Install with: npm install tw-animate-css');
880
+ warn(`${PACKAGE_NAMES.TW_ANIMATE_CSS} package not found - removed from CSS`);
881
+ info(` Install with: ${installCmd}`);
786
882
  info(' Or add the import manually after installing the package');
787
883
  }
788
884
  }
789
885
  } else {
790
886
  // Package is installed - ensure import exists and remove duplicates
791
887
  processed = removeDuplicateTwAnimateImports(processed);
792
-
888
+
793
889
  // Add import if it doesn't exist (right after tailwindcss if present)
794
- if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
795
- if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
890
+ if (!processed.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
891
+ if (hasTailwindImport(processed)) {
796
892
  processed = processed.replace(
797
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
893
+ CSS_PATTERNS.TAILWIND_IMPORT,
798
894
  '$1@import "tw-animate-css";\n'
799
895
  );
800
896
  } else {
@@ -802,7 +898,7 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
802
898
  }
803
899
  }
804
900
  }
805
-
901
+
806
902
  return processed;
807
903
  }
808
904
 
@@ -814,7 +910,7 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
814
910
  * @returns {string} CSS content without tailwindcss imports
815
911
  */
816
912
  function removeTailwindImport(cssContent) {
817
- return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
913
+ return cssContent.replace(CSS_PATTERNS.TAILWIND_IMPORT, '');
818
914
  }
819
915
 
820
916
  /**
@@ -829,23 +925,23 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
829
925
  if (!hasTwAnimate) {
830
926
  return cssContent;
831
927
  }
832
-
928
+
833
929
  // Remove duplicates first
834
930
  let cleaned = removeDuplicateTwAnimateImports(cssContent);
835
-
931
+
836
932
  // Check if import already exists
837
- if (cleaned.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
933
+ if (cleaned.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
838
934
  return cleaned;
839
935
  }
840
-
936
+
841
937
  // Add import after tailwindcss if it exists, otherwise at the beginning
842
- if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
938
+ if (hasTailwindImport(cleaned)) {
843
939
  return cleaned.replace(
844
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
940
+ CSS_PATTERNS.TAILWIND_IMPORT,
845
941
  '$1@import "tw-animate-css";\n'
846
942
  );
847
943
  }
848
-
944
+
849
945
  return '@import "tw-animate-css";\n' + cleaned;
850
946
  }
851
947
 
@@ -863,35 +959,32 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
863
959
  * @returns {string} Merged CSS content
864
960
  */
865
961
  function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
866
- const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
962
+ const tailwindMatch = existing.match(CSS_PATTERNS.TAILWIND_IMPORT);
867
963
  if (!tailwindMatch) {
868
964
  // No tailwindcss import found - just prepend Weloop styles
869
965
  return weloopStyles + '\n\n' + existing;
870
966
  }
871
-
967
+
872
968
  // Split existing CSS around the tailwindcss import
873
- const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
874
- const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
875
-
969
+ const tailwindIndex = existing.indexOf(tailwindMatch[0]);
970
+ const beforeTailwind = existing.substring(0, tailwindIndex);
971
+ const afterTailwind = existing.substring(tailwindIndex + tailwindMatch[0].length);
972
+
876
973
  // Check if tw-animate-css import already exists
877
- const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
878
- const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
879
-
974
+ const hasTwAnimateInExisting = existing.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
975
+ const hasTwAnimateInWeloop = weloopStyles.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
976
+
880
977
  // If existing file already has tw-animate import, remove it from Weloop styles
881
978
  // to avoid duplicating it in the merge output
882
979
  let weloopStylesCleaned = weloopStyles;
883
980
  if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
884
- weloopStylesCleaned = weloopStyles.replace(
885
- /@import\s+["']tw-animate-css["'];?\s*\n?/g,
886
- ''
887
- );
981
+ weloopStylesCleaned = weloopStyles.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
888
982
  }
889
-
983
+
890
984
  // Add tw-animate import if needed (package installed but import missing)
891
- let importsToAdd = '';
892
- if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
893
- importsToAdd = '@import "tw-animate-css";\n';
894
- }
985
+ const importsToAdd = (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop)
986
+ ? '@import "tw-animate-css";\n'
987
+ : '';
895
988
 
896
989
  // Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
897
990
  return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
@@ -910,28 +1003,26 @@ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
910
1003
  * @returns {string} CSS content with Weloop styles replaced
911
1004
  */
912
1005
  function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
913
- const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
914
- const imports = existing.match(importPattern) || [];
1006
+ const imports = existing.match(CSS_PATTERNS.IMPORT_STATEMENT) || [];
915
1007
  const importsText = imports.join('');
916
-
1008
+
917
1009
  // Find where Weloop styles start (look for @theme inline or :root)
918
- const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
919
- const weloopStartMatch = existing.search(weloopStartPattern);
920
-
1010
+ const weloopStartMatch = existing.search(CSS_PATTERNS.WELOOP_START);
1011
+
921
1012
  if (weloopStartMatch === -1) {
922
1013
  // No existing Weloop styles found - just append
923
1014
  return existing + '\n\n' + newStyles;
924
1015
  }
925
-
1016
+
926
1017
  // Extract content before Weloop styles
927
1018
  let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
928
1019
  // Keep non-Weloop imports (filter out tw-animate if package not installed)
929
- const nonWeloopImports = importsText.split('\n').filter(imp =>
1020
+ const nonWeloopImports = importsText.split('\n').filter(imp =>
930
1021
  !imp.includes('tw-animate-css') || hasTwAnimate
931
1022
  ).join('\n');
932
1023
  // Remove all imports from contentBeforeWeloop (we'll add them back)
933
- contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
934
-
1024
+ contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(CSS_PATTERNS.IMPORT_STATEMENT, '');
1025
+
935
1026
  // Return: preserved imports + content before Weloop + new Weloop styles
936
1027
  return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
937
1028
  }
@@ -947,11 +1038,11 @@ function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
947
1038
  */
948
1039
  async function fetchCSSFromRegistry(registryUrl) {
949
1040
  let sourceCssPath;
950
-
1041
+
951
1042
  if (isLocalPath(registryUrl)) {
952
1043
  // Local path: find app/globals.css relative to registry directory
953
- const basePath = path.isAbsolute(registryUrl)
954
- ? path.dirname(registryUrl)
1044
+ const basePath = path.isAbsolute(registryUrl)
1045
+ ? path.dirname(registryUrl)
955
1046
  : path.join(process.cwd(), path.dirname(registryUrl));
956
1047
  sourceCssPath = path.join(basePath, 'app', 'globals.css');
957
1048
  } else {
@@ -959,7 +1050,7 @@ async function fetchCSSFromRegistry(registryUrl) {
959
1050
  const baseUrl = registryUrl.replace('/registry', '');
960
1051
  sourceCssPath = `${baseUrl}/app/globals.css`;
961
1052
  }
962
-
1053
+
963
1054
  return await fetchText(sourceCssPath);
964
1055
  }
965
1056
 
@@ -977,52 +1068,49 @@ async function fetchCSSFromRegistry(registryUrl) {
977
1068
  * @param {boolean} silent - Whether to suppress output messages
978
1069
  */
979
1070
  async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
980
- const cssPath = config.tailwind?.css || 'app/globals.css';
1071
+ const cssPath = config.tailwind?.css || DEFAULTS.CSS_PATH;
981
1072
  const fullCssPath = path.join(process.cwd(), cssPath);
982
-
1073
+
983
1074
  // Check if Weloop styles already exist in the file
984
1075
  let hasWeloopStylesInFile = false;
985
1076
  if (fs.existsSync(fullCssPath)) {
986
1077
  const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
987
1078
  hasWeloopStylesInFile = hasWeloopStyles(existingContent);
988
-
1079
+
989
1080
  // If styles already exist and we're not forcing update, skip silently
990
1081
  // This prevents unnecessary updates when installing components
991
1082
  if (hasWeloopStylesInFile && !forceUpdate && silent) {
992
1083
  return;
993
1084
  }
994
1085
  }
995
-
1086
+
996
1087
  try {
997
1088
  if (!silent) {
998
1089
  info('Installing CSS styles...');
999
1090
  }
1000
-
1091
+
1001
1092
  const cssContent = await fetchCSSFromRegistry(registryUrl);
1002
1093
  ensureDirectoryExists(path.dirname(fullCssPath));
1003
-
1004
- const hasTwAnimate = checkPackageInstalled('tw-animate-css');
1094
+
1095
+ const hasTwAnimate = checkPackageInstalled(PACKAGE_NAMES.TW_ANIMATE_CSS);
1005
1096
  let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
1006
-
1097
+
1007
1098
  // Handle file installation based on mode and existing file state
1008
1099
  if (forceUpdate && fs.existsSync(fullCssPath)) {
1009
1100
  // Mode 1: --overwrite flag - Replace entire file with fresh styles
1010
- let normalized = normalizeCSSFormat(processedCssContent);
1011
- normalized = removeDuplicateTwAnimateImports(normalized);
1012
- normalized = removeDuplicateCustomVariant(normalized);
1101
+ const normalized = normalizeAndCleanCSS(processedCssContent, hasTwAnimate);
1013
1102
  fs.writeFileSync(fullCssPath, normalized);
1014
1103
  if (!silent) {
1015
1104
  success(`Overwritten ${cssPath} with Weloop styles`);
1016
1105
  if (hasTwAnimate) {
1017
- info(` tw-animate-css import included`);
1106
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1018
1107
  }
1019
1108
  }
1020
1109
  } else if (fs.existsSync(fullCssPath)) {
1021
1110
  // Mode 2: Normal update - Intelligently merge with existing styles
1022
1111
  const existing = fs.readFileSync(fullCssPath, 'utf-8');
1023
- const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
1024
- existing.includes('@tailwind base');
1025
-
1112
+ const existingHasTailwind = hasTailwindImport(existing);
1113
+
1026
1114
  if (hasWeloopStylesInFile) {
1027
1115
  // Case 2a: Weloop styles already exist - replace them with updated versions
1028
1116
  let weloopStyles = removeTailwindImport(processedCssContent);
@@ -1031,7 +1119,7 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1031
1119
  finalContent = removeDuplicateTwAnimateImports(finalContent);
1032
1120
  finalContent = removeDuplicateCustomVariant(finalContent);
1033
1121
  finalContent = normalizeCSSFormat(finalContent);
1034
-
1122
+
1035
1123
  // Only write if content actually changed (prevents unnecessary file updates)
1036
1124
  if (finalContent !== existing) {
1037
1125
  fs.writeFileSync(fullCssPath, finalContent);
@@ -1040,31 +1128,29 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1040
1128
  }
1041
1129
  }
1042
1130
  // If no changes, silently skip (file is already up to date)
1043
- } else if (hasTailwindImport) {
1131
+ } else if (existingHasTailwind) {
1044
1132
  // Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
1045
1133
  let weloopStyles = removeTailwindImport(processedCssContent);
1046
1134
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
1047
1135
  let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
1048
- merged = removeDuplicateTwAnimateImports(merged);
1049
- merged = removeDuplicateCustomVariant(merged);
1050
- merged = normalizeCSSFormat(merged);
1136
+ merged = normalizeAndCleanCSS(merged, hasTwAnimate);
1051
1137
  fs.writeFileSync(fullCssPath, merged);
1052
1138
  if (!silent) {
1053
1139
  success(`Updated ${cssPath} with Weloop styles`);
1054
1140
  if (hasTwAnimate) {
1055
- info(` tw-animate-css import included`);
1141
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1056
1142
  }
1057
1143
  info(` Your existing styles are preserved`);
1058
1144
  }
1059
1145
  } else {
1060
1146
  // Case 2c: No Tailwind imports - prepend Weloop styles to existing content
1061
1147
  let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
1062
-
1148
+
1063
1149
  // Ensure tw-animate import exists if package is installed
1064
- if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
1065
- if (finalCssContent.includes('@import "tailwindcss"')) {
1150
+ if (hasTwAnimate && !finalCssContent.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
1151
+ if (hasTailwindImport(finalCssContent)) {
1066
1152
  finalCssContent = finalCssContent.replace(
1067
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
1153
+ CSS_PATTERNS.TAILWIND_IMPORT,
1068
1154
  '$1@import "tw-animate-css";\n'
1069
1155
  );
1070
1156
  } else {
@@ -1072,35 +1158,34 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1072
1158
  }
1073
1159
  }
1074
1160
  let combined = finalCssContent + '\n\n' + existing;
1075
- combined = normalizeCSSFormat(combined);
1161
+ combined = normalizeAndCleanCSS(combined, hasTwAnimate);
1076
1162
  fs.writeFileSync(fullCssPath, combined);
1077
1163
  if (!silent) {
1078
1164
  success(`Updated ${cssPath} with Weloop styles`);
1079
1165
  if (hasTwAnimate) {
1080
- info(` tw-animate-css import included`);
1166
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1081
1167
  }
1082
1168
  }
1083
1169
  }
1084
1170
  } else {
1085
1171
  // Mode 3: File doesn't exist - create new file with Weloop styles
1086
- let normalized = normalizeCSSFormat(processedCssContent);
1087
- normalized = removeDuplicateTwAnimateImports(normalized);
1088
- normalized = removeDuplicateCustomVariant(normalized);
1172
+ const normalized = normalizeAndCleanCSS(processedCssContent, hasTwAnimate);
1089
1173
  fs.writeFileSync(fullCssPath, normalized);
1090
1174
  if (!silent) {
1091
1175
  success(`Created ${cssPath} with Weloop styles`);
1092
1176
  if (hasTwAnimate) {
1093
- info(` tw-animate-css import included`);
1177
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1094
1178
  }
1095
1179
  }
1096
1180
  }
1097
1181
  } catch (err) {
1098
1182
  if (!silent) {
1099
1183
  warn(`Could not automatically install CSS styles: ${err.message}`);
1100
- info(`\n To add styles manually:`);
1101
- info(` 1. Download: ${registryUrl.replace('/registry', '/app/globals.css')}`);
1102
- info(` 2. Add the CSS variables to your ${cssPath} file`);
1103
- info(` 3. Or copy from: https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/app/globals.css\n`);
1184
+ info(getCSSInstallErrorMessage(registryUrl, cssPath));
1185
+ }
1186
+ // Re-throw in silent mode so caller can handle it
1187
+ if (silent) {
1188
+ throw err;
1104
1189
  }
1105
1190
  }
1106
1191
  }
@@ -1119,11 +1204,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1119
1204
  */
1120
1205
  function createUtilsFile(utilsPath) {
1121
1206
  if (fs.existsSync(utilsPath)) return;
1122
-
1207
+
1123
1208
  // Silently create utils file (matching shadcn/ui behavior)
1124
1209
  // Don't overwrite if user has customized it
1125
1210
  ensureDirectoryExists(path.dirname(utilsPath));
1126
-
1211
+
1127
1212
  const utilsContent = `import { clsx, type ClassValue } from "clsx"
1128
1213
  import { twMerge } from "tailwind-merge"
1129
1214
 
@@ -1153,47 +1238,41 @@ export function cn(...inputs: ClassValue[]) {
1153
1238
  */
1154
1239
  async function installComponent(componentName, options = {}) {
1155
1240
  const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
1156
-
1241
+
1157
1242
  // Step 1: Install base dependencies (required for utils.ts to work)
1158
- const baseDeps = [];
1159
- if (!checkPackageInstalled('clsx')) {
1160
- baseDeps.push('clsx');
1161
- }
1162
- if (!checkPackageInstalled('tailwind-merge')) {
1163
- baseDeps.push('tailwind-merge');
1164
- }
1243
+ const baseDeps = BASE_DEPENDENCIES.filter(dep => !checkPackageInstalled(dep));
1165
1244
  if (baseDeps.length > 0) {
1166
1245
  info(`Installing base dependencies: ${baseDeps.join(', ')}...`);
1167
1246
  await installPackages(baseDeps);
1168
1247
  }
1169
-
1248
+
1170
1249
  // Step 2: Load or create components.json configuration
1171
1250
  const config = loadComponentsConfig();
1172
-
1251
+
1173
1252
  // Step 3: Install tw-animate-css automatically (required for component animations)
1174
- if (!checkPackageInstalled('tw-animate-css')) {
1175
- info('Installing tw-animate-css for animations...');
1176
- await installPackages(['tw-animate-css']);
1253
+ if (!checkPackageInstalled(PACKAGE_NAMES.TW_ANIMATE_CSS)) {
1254
+ info(`Installing ${PACKAGE_NAMES.TW_ANIMATE_CSS} for animations...`);
1255
+ await installPackages([PACKAGE_NAMES.TW_ANIMATE_CSS]);
1177
1256
  }
1178
-
1257
+
1179
1258
  // Step 4: Install CSS styles early (before component installation)
1180
1259
  // Silent mode prevents output when styles are already installed
1181
1260
  await installCSSStyles(config, registryUrl, false, true);
1182
-
1261
+
1183
1262
  // Step 5: Resolve paths from components.json configuration
1184
1263
  const uiAlias = config.aliases?.ui || '@/components/ui';
1185
1264
  const utilsAlias = config.aliases?.utils || '@/lib/utils';
1186
-
1265
+
1187
1266
  const componentsDir = path.join(process.cwd(), uiAlias.replace('@/', '').replace(/^\/+/, ''));
1188
1267
  let utilsPath = utilsAlias.replace('@/', '').replace(/^\/+/, '');
1189
1268
  if (!utilsPath.endsWith('.ts') && !utilsPath.endsWith('.tsx')) {
1190
1269
  utilsPath = utilsPath + '.ts';
1191
1270
  }
1192
1271
  utilsPath = path.join(process.cwd(), utilsPath);
1193
-
1272
+
1194
1273
  // Step 6: Load component registry from remote or local source
1195
1274
  const registry = await loadRegistry(componentName, registryUrl);
1196
-
1275
+
1197
1276
  if (!registry) {
1198
1277
  error(`Component "${componentName}" not found in registry.`);
1199
1278
  info('Available components:');
@@ -1263,20 +1342,20 @@ async function installComponent(componentName, options = {}) {
1263
1342
  function detectProjectType() {
1264
1343
  // Check for Next.js
1265
1344
  if (fs.existsSync(path.join(process.cwd(), 'next.config.js')) ||
1266
- fs.existsSync(path.join(process.cwd(), 'next.config.mjs')) ||
1267
- fs.existsSync(path.join(process.cwd(), 'next.config.ts')) ||
1268
- fs.existsSync(path.join(process.cwd(), 'app')) ||
1269
- fs.existsSync(path.join(process.cwd(), 'pages'))) {
1345
+ fs.existsSync(path.join(process.cwd(), 'next.config.mjs')) ||
1346
+ fs.existsSync(path.join(process.cwd(), 'next.config.ts')) ||
1347
+ fs.existsSync(path.join(process.cwd(), 'app')) ||
1348
+ fs.existsSync(path.join(process.cwd(), 'pages'))) {
1270
1349
  return 'nextjs';
1271
1350
  }
1272
-
1351
+
1273
1352
  // Check for Vite
1274
1353
  if (fs.existsSync(path.join(process.cwd(), 'vite.config.js')) ||
1275
- fs.existsSync(path.join(process.cwd(), 'vite.config.ts')) ||
1276
- fs.existsSync(path.join(process.cwd(), 'vite.config.mjs'))) {
1354
+ fs.existsSync(path.join(process.cwd(), 'vite.config.ts')) ||
1355
+ fs.existsSync(path.join(process.cwd(), 'vite.config.mjs'))) {
1277
1356
  return 'vite';
1278
1357
  }
1279
-
1358
+
1280
1359
  return null;
1281
1360
  }
1282
1361
 
@@ -1293,11 +1372,11 @@ function prompt(question, defaultValue = '') {
1293
1372
  input: process.stdin,
1294
1373
  output: process.stdout
1295
1374
  });
1296
-
1297
- const promptText = defaultValue
1375
+
1376
+ const promptText = defaultValue
1298
1377
  ? `${question} › ${defaultValue} `
1299
1378
  : `${question} › `;
1300
-
1379
+
1301
1380
  rl.question(promptText, (answer) => {
1302
1381
  rl.close();
1303
1382
  resolve(answer.trim() || defaultValue);
@@ -1306,73 +1385,439 @@ function prompt(question, defaultValue = '') {
1306
1385
  }
1307
1386
 
1308
1387
  /**
1309
- * Initializes the project with components.json configuration
1310
- * Similar to shadcn/ui init command
1388
+ * Initializes the project with interactive prompts
1389
+ * Similar to shadcn/ui init command but with custom theme selection
1311
1390
  */
1312
1391
  async function initProject() {
1313
1392
  info('Initializing project...\n');
1314
-
1393
+
1315
1394
  // Check if components.json already exists
1316
- const configPath = path.join(process.cwd(), 'components.json');
1395
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
1317
1396
  if (fs.existsSync(configPath)) {
1318
- warn('components.json already exists.');
1397
+ warn(`${PATHS.COMPONENTS_JSON} already exists.`);
1319
1398
  const overwrite = await prompt('Do you want to overwrite it? (y/N)', 'N');
1320
1399
  if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
1321
1400
  info('Skipping initialization.');
1322
1401
  return;
1323
1402
  }
1324
1403
  }
1325
-
1326
- // Detect project type
1327
- const projectType = detectProjectType();
1328
- let detectedType = projectType || 'nextjs';
1329
-
1330
- if (!projectType) {
1331
- info('Could not detect project type. Defaulting to Next.js.');
1332
- } else {
1333
- info(`Detected ${projectType === 'nextjs' ? 'Next.js' : 'Vite'} project.`);
1404
+
1405
+ // Step 1: Ask for project type
1406
+ console.log('Project ? Next.js | Vite');
1407
+ const projectTypeAnswer = await prompt('Project ?', 'Next.js');
1408
+ const isNextjs = projectTypeAnswer.toLowerCase().includes('next');
1409
+ const isVite = projectTypeAnswer.toLowerCase().includes('vite');
1410
+
1411
+ // Step 2: Ask for project name
1412
+ console.log('\nProject Name ? name-app | (default: my-app)');
1413
+ const projectName = await prompt('Project Name ?', 'my-app');
1414
+
1415
+ // Step 3: Ask for theme color with options
1416
+ console.log('\nTheme Color ? (Blue: Default, Tech-Blue and Brew) for user to choose');
1417
+ console.log('Available themes:');
1418
+ console.log(' 1. Blue (Default)');
1419
+ console.log(' 2. Tech-Blue');
1420
+ console.log(' 3. Brew');
1421
+ const themeAnswer = await prompt('Theme Color ?', '1');
1422
+
1423
+ let selectedTheme;
1424
+ switch (themeAnswer) {
1425
+ case '2':
1426
+ case 'tech-blue':
1427
+ case 'Tech-Blue':
1428
+ selectedTheme = 'tech-blue';
1429
+ break;
1430
+ case '3':
1431
+ case 'brew':
1432
+ case 'Brew':
1433
+ selectedTheme = 'brew';
1434
+ break;
1435
+ case '1':
1436
+ case 'blue':
1437
+ case 'Blue':
1438
+ default:
1439
+ selectedTheme = 'blue';
1440
+ break;
1334
1441
  }
1442
+
1443
+ // Step 4: Ask for border radius
1444
+ console.log('\nBorder Radius ? Choose the border radius for components');
1445
+ console.log('Available options:');
1446
+ console.log(' 1. 0px (Sharp)');
1447
+ console.log(' 2. 4px (Small)');
1448
+ console.log(' 3. 8px (Default)');
1449
+ console.log(' 4. 10px (Medium)');
1450
+ console.log(' 5. 16px (Large)');
1451
+ const radiusAnswer = await prompt('Border Radius ?', '3');
1452
+
1453
+ let selectedRadius;
1454
+ switch (radiusAnswer) {
1455
+ case '1':
1456
+ case '0':
1457
+ case '0px':
1458
+ selectedRadius = '0px';
1459
+ break;
1460
+ case '2':
1461
+ case '4':
1462
+ case '4px':
1463
+ selectedRadius = '4px';
1464
+ break;
1465
+ case '4':
1466
+ case '10':
1467
+ case '10px':
1468
+ selectedRadius = '10px';
1469
+ break;
1470
+ case '5':
1471
+ case '16':
1472
+ case '16px':
1473
+ selectedRadius = '16px';
1474
+ break;
1475
+ case '3':
1476
+ case '8':
1477
+ case '8px':
1478
+ default:
1479
+ selectedRadius = '8px';
1480
+ break;
1481
+ }
1482
+
1483
+ // Show configuration summary
1484
+ info('\nšŸ“‹ Configuration Summary:');
1485
+ info(` Project Type: ${isNextjs ? 'Next.js' : 'Vite'}`);
1486
+ info(` Project Name: ${projectName}`);
1487
+ info(` Theme Color: ${selectedTheme}`);
1488
+ info(` Border Radius: ${selectedRadius}`);
1489
+
1490
+ const confirm = await prompt('\nProceed with this configuration? (Y/n)', 'Y');
1491
+ if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
1492
+ info('Initialization cancelled.');
1493
+ return;
1494
+ }
1495
+
1496
+ // Step 3: Setup project structure based on shadcn/ui + Next.js
1497
+ info('\nšŸ—ļø Setting up project structure...');
1498
+
1499
+ // Create basic Next.js project structure
1500
+ const directories = [
1501
+ 'app',
1502
+ 'components',
1503
+ 'components/ui',
1504
+ 'lib',
1505
+ 'hooks',
1506
+ 'types',
1507
+ 'public',
1508
+ 'public/icons'
1509
+ ];
1510
+
1511
+ directories.forEach(dir => {
1512
+ const dirPath = path.join(process.cwd(), dir);
1513
+ if (!fs.existsSync(dirPath)) {
1514
+ fs.mkdirSync(dirPath, { recursive: true });
1515
+ info(` → Created ${dir}/ directory`);
1516
+ }
1517
+ });
1518
+
1519
+ // Create Next.js app structure files
1520
+ const appFiles = {
1521
+ 'app/layout.tsx': `import type { Metadata } from "next";
1522
+ import { Inter } from "next/font/google";
1523
+ import "./globals.css";
1524
+
1525
+ const inter = Inter({ subsets: ["latin"] });
1526
+
1527
+ export const metadata: Metadata = {
1528
+ title: "${projectName}",
1529
+ description: "Built with Weloop Design System",
1530
+ };
1531
+
1532
+ export default function RootLayout({
1533
+ children,
1534
+ }: {
1535
+ children: React.ReactNode;
1536
+ }) {
1537
+ return (
1538
+ <html lang="en">
1539
+ <body className={inter.className}>{children}</body>
1540
+ </html>
1541
+ );
1542
+ }`,
1543
+ 'app/page.tsx': `export default function Home() {
1544
+ return (
1545
+ <main className="flex min-h-screen flex-col items-center justify-between p-24">
1546
+ <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
1547
+ <h1 className="text-4xl font-bold">
1548
+ Welcome to ${projectName}
1549
+ </h1>
1550
+ </div>
1551
+ </main>
1552
+ );
1553
+ }`,
1554
+ 'app/globals.css': `@tailwind base;
1555
+ @tailwind components;
1556
+ @tailwind utilities;
1557
+
1558
+ @layer base {
1559
+ :root {
1560
+ --background: 0 0% 100%;
1561
+ --foreground: 222.2 84% 4.9%;
1562
+ --card: 0 0% 100%;
1563
+ --card-foreground: 222.2 84% 4.9%;
1564
+ --popover: 0 0% 100%;
1565
+ --popover-foreground: 222.2 84% 4.9%;
1566
+ --primary: 222.2 47.4% 11.2%;
1567
+ --primary-foreground: 210 40% 98%;
1568
+ --secondary: 210 40% 96%;
1569
+ --secondary-foreground: 222.2 84% 4.9%;
1570
+ --muted: 210 40% 96%;
1571
+ --muted-foreground: 215.4 16.3% 46.9%;
1572
+ --accent: 210 40% 96%;
1573
+ --accent-foreground: 222.2 84% 4.9%;
1574
+ --destructive: 0 84.2% 60.2%;
1575
+ --destructive-foreground: 210 40% 98%;
1576
+ --border: 214.3 31.8% 91.4%;
1577
+ --input: 214.3 31.8% 91.4%;
1578
+ --ring: 222.2 84% 4.9%;
1579
+ --radius: ${selectedRadius.replace('px', '')};
1580
+ --chart-1: 12 76% 61%;
1581
+ --chart-2: 173 58% 39%;
1582
+ --chart-3: 197 37% 24%;
1583
+ --chart-4: 43 74% 66%;
1584
+ --chart-5: 27 87% 67%;
1585
+ }
1586
+
1587
+ .dark {
1588
+ --background: 222.2 84% 4.9%;
1589
+ --foreground: 210 40% 98%;
1590
+ --card: 222.2 84% 4.9%;
1591
+ --card-foreground: 210 40% 98%;
1592
+ --popover: 222.2 84% 4.9%;
1593
+ --popover-foreground: 210 40% 98%;
1594
+ --primary: 210 40% 98%;
1595
+ --primary-foreground: 222.2 47.4% 11.2%;
1596
+ --secondary: 217.2 32.6% 17.5%;
1597
+ --secondary-foreground: 210 40% 98%;
1598
+ --muted: 217.2 32.6% 17.5%;
1599
+ --muted-foreground: 215 20.2% 65.1%;
1600
+ --accent: 217.2 32.6% 17.5%;
1601
+ --accent-foreground: 210 40% 98%;
1602
+ --destructive: 0 62.8% 30.6%;
1603
+ --destructive-foreground: 210 40% 98%;
1604
+ --border: 217.2 32.6% 17.5%;
1605
+ --input: 217.2 32.6% 17.5%;
1606
+ --ring: 212.7 26.8% 83.9%;
1607
+ --chart-1: 220 70% 50%;
1608
+ --chart-2: 160 60% 45%;
1609
+ --chart-3: 30 80% 55%;
1610
+ --chart-4: 280 65% 60%;
1611
+ --chart-5: 340 75% 55%;
1612
+ }
1613
+ }
1614
+
1615
+ @layer base {
1616
+ * {
1617
+ @apply border-border;
1618
+ }
1619
+ body {
1620
+ @apply bg-background text-foreground;
1621
+ }
1622
+ }`,
1623
+ 'next.config.mjs': `/** @type {import('next').NextConfig} */
1624
+ const nextConfig = {
1625
+ experimental: {
1626
+ appDir: true,
1627
+ },
1628
+ };
1629
+
1630
+ export default nextConfig;`,
1631
+ 'tailwind.config.ts': `import type { Config } from "tailwindcss";
1632
+
1633
+ const config: Config = {
1634
+ darkMode: ["class"],
1635
+ content: [
1636
+ "./pages/**/*.{ts,tsx}",
1637
+ "./components/**/*.{ts,tsx}",
1638
+ "./app/**/*.{ts,tsx}",
1639
+ "./src/**/*.{ts,tsx}",
1640
+ ],
1641
+ prefix: "",
1642
+ theme: {
1643
+ container: {
1644
+ center: true,
1645
+ padding: "2rem",
1646
+ screens: {
1647
+ "2xl": "1400px",
1648
+ },
1649
+ },
1650
+ extend: {
1651
+ colors: {
1652
+ border: "hsl(var(--border))",
1653
+ input: "hsl(var(--input))",
1654
+ ring: "hsl(var(--ring))",
1655
+ background: "hsl(var(--background))",
1656
+ foreground: "hsl(var(--foreground))",
1657
+ primary: {
1658
+ DEFAULT: "hsl(var(--primary))",
1659
+ foreground: "hsl(var(--primary-foreground))",
1660
+ },
1661
+ secondary: {
1662
+ DEFAULT: "hsl(var(--secondary))",
1663
+ foreground: "hsl(var(--secondary-foreground))",
1664
+ },
1665
+ destructive: {
1666
+ DEFAULT: "hsl(var(--destructive))",
1667
+ foreground: "hsl(var(--destructive-foreground))",
1668
+ },
1669
+ muted: {
1670
+ DEFAULT: "hsl(var(--muted))",
1671
+ foreground: "hsl(var(--muted-foreground))",
1672
+ },
1673
+ accent: {
1674
+ DEFAULT: "hsl(var(--accent))",
1675
+ foreground: "hsl(var(--accent-foreground))",
1676
+ },
1677
+ popover: {
1678
+ DEFAULT: "hsl(var(--popover))",
1679
+ foreground: "hsl(var(--popover-foreground))",
1680
+ },
1681
+ card: {
1682
+ DEFAULT: "hsl(var(--card))",
1683
+ foreground: "hsl(var(--card-foreground))",
1684
+ },
1685
+ },
1686
+ borderRadius: {
1687
+ lg: "var(--radius)",
1688
+ md: "calc(var(--radius) - 2px)",
1689
+ sm: "calc(var(--radius) - 4px)",
1690
+ },
1691
+ keyframes: {
1692
+ "accordion-down": {
1693
+ from: { height: "0" },
1694
+ to: { height: "var(--radix-accordion-content-height)" },
1695
+ },
1696
+ "accordion-up": {
1697
+ from: { height: "var(--radix-accordion-content-height)" },
1698
+ to: { height: "0" },
1699
+ },
1700
+ },
1701
+ animation: {
1702
+ "accordion-down": "accordion-down 0.2s ease-out",
1703
+ "accordion-up": "accordion-up 0.2s ease-out",
1704
+ },
1705
+ },
1706
+ },
1707
+ plugins: [require("tailwindcss-animate")],
1708
+ } satisfies Config;
1709
+
1710
+ export default config;`,
1711
+ 'tsconfig.json': `{
1712
+ "compilerOptions": {
1713
+ "target": "es5",
1714
+ "lib": ["dom", "dom.iterable", "es6"],
1715
+ "allowJs": true,
1716
+ "skipLibCheck": true,
1717
+ "strict": true,
1718
+ "noEmit": true,
1719
+ "esModuleInterop": true,
1720
+ "module": "esnext",
1721
+ "moduleResolution": "bundler",
1722
+ "resolveJsonModule": true,
1723
+ "isolatedModules": true,
1724
+ "jsx": "preserve",
1725
+ "incremental": true,
1726
+ "plugins": [
1727
+ {
1728
+ "name": "next"
1729
+ }
1730
+ ],
1731
+ "baseUrl": ".",
1732
+ "paths": {
1733
+ "@/*": ["./*"]
1734
+ }
1735
+ },
1736
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1737
+ "exclude": ["node_modules"]
1738
+ }`,
1739
+ '.eslintrc.json': `{
1740
+ "extends": ["next/core-web-vitals", "next/typescript"]
1741
+ }`
1742
+ };
1743
+
1744
+ // Create files
1745
+ Object.entries(appFiles).forEach(([filePath, content]) => {
1746
+ const fullPath = path.join(process.cwd(), filePath);
1747
+ if (!fs.existsSync(fullPath)) {
1748
+ fs.writeFileSync(fullPath, content);
1749
+ info(` → Created ${filePath}`);
1750
+ }
1751
+ });
1752
+
1753
+ // Create or update package.json for Next.js project
1754
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
1755
+ let packageJson = { dependencies: {}, devDependencies: {} };
1335
1756
 
1336
- // Ask for project type if not detected or user wants to override
1337
- const useDetected = projectType
1338
- ? await prompt(`Which project type? (Next.js/Vite)`, detectedType === 'nextjs' ? 'Next.js' : 'Vite')
1339
- : await prompt(`Which project type? (Next.js/Vite)`, 'Next.js');
1340
-
1341
- const isNextjs = useDetected.toLowerCase().includes('next');
1342
- const isVite = useDetected.toLowerCase().includes('vite');
1343
-
1344
- // Detect CSS path
1345
- let cssPath = 'app/globals.css';
1757
+ if (fs.existsSync(packageJsonPath)) {
1758
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
1759
+ }
1760
+
1761
+ // Update package.json with Next.js dependencies
1762
+ const nextDependencies = {
1763
+ "next": "^14.0.0",
1764
+ "react": "^18.0.0",
1765
+ "react-dom": "^18.0.0",
1766
+ "typescript": "^5.0.0",
1767
+ "@types/node": "^20.0.0",
1768
+ "@types/react": "^18.0.0",
1769
+ "@types/react-dom": "^18.0.0",
1770
+ "tailwindcss": "^3.3.0",
1771
+ "autoprefixer": "^10.0.0",
1772
+ "postcss": "^8.0.0",
1773
+ "tailwindcss-animate": "^1.0.7",
1774
+ "eslint": "^8.0.0",
1775
+ "eslint-config-next": "^14.0.0"
1776
+ };
1777
+
1778
+ packageJson.name = projectName;
1779
+ packageJson.version = "0.1.0";
1780
+ packageJson.private = true;
1781
+ packageJson.scripts = {
1782
+ "dev": "next dev",
1783
+ "build": "next build",
1784
+ "start": "next start",
1785
+ "lint": "next lint"
1786
+ };
1787
+
1788
+ // Merge dependencies
1789
+ Object.assign(packageJson.dependencies || {}, nextDependencies);
1790
+ Object.assign(packageJson.devDependencies || {}, {});
1791
+
1792
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
1793
+ info(` → Updated package.json`);
1794
+
1795
+ // Detect CSS path based on project type
1796
+ let cssPath = PATHS.APP_GLOBALS_CSS;
1346
1797
  if (isVite) {
1347
- cssPath = 'src/index.css';
1798
+ cssPath = PATHS.VITE_INDEX_CSS;
1348
1799
  } else if (fs.existsSync(path.join(process.cwd(), 'app', 'globals.css'))) {
1349
- cssPath = 'app/globals.css';
1800
+ cssPath = PATHS.APP_GLOBALS_CSS;
1350
1801
  } else if (fs.existsSync(path.join(process.cwd(), 'styles', 'globals.css'))) {
1351
- cssPath = 'styles/globals.css';
1802
+ cssPath = PATHS.STYLES_GLOBALS_CSS;
1352
1803
  } else {
1353
1804
  cssPath = await prompt('Where is your global CSS file?', cssPath);
1354
1805
  }
1355
-
1356
- // Ask for base color
1357
- const baseColor = await prompt('Which color would you like to use as base color?', 'neutral');
1358
-
1359
- // Ask for style
1360
- const style = await prompt('Which style would you like to use?', 'blue');
1361
-
1362
- // Create components.json
1806
+
1807
+ // Create components.json with selected theme
1363
1808
  const config = {
1364
- $schema: 'https://weloop.kosign.dev/schema.json',
1365
- style: style,
1809
+ $schema: DEFAULTS.SCHEMA_URL,
1810
+ style: selectedTheme,
1366
1811
  rsc: isNextjs,
1367
1812
  tsx: true,
1368
1813
  tailwind: {
1369
1814
  config: 'tailwind.config.ts',
1370
1815
  css: cssPath,
1371
- baseColor: baseColor,
1816
+ baseColor: 'neutral',
1372
1817
  cssVariables: true,
1373
1818
  prefix: ''
1374
1819
  },
1375
- iconLibrary: 'lucide',
1820
+ iconLibrary: DEFAULTS.ICON_LIBRARY,
1376
1821
  aliases: {
1377
1822
  components: '@/components',
1378
1823
  utils: '@/lib/utils',
@@ -1380,35 +1825,53 @@ async function initProject() {
1380
1825
  lib: '@/lib',
1381
1826
  hooks: '@/hooks'
1382
1827
  },
1383
- registry: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry/index.json'
1828
+ registry: REGISTRY_URLS.INDEX,
1829
+ projectName: projectName,
1830
+ radius: selectedRadius
1384
1831
  };
1385
-
1832
+
1833
+ // Show setup progress
1834
+ info('\nšŸš€ Setting up your project...');
1835
+
1836
+ // Step 1: Create components.json
1837
+ info(' → [:root]/components.json');
1386
1838
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1387
- success('Created components.json\n');
1388
-
1389
- // Install base dependencies
1390
- info('Installing base dependencies...');
1391
- const baseDeps = ['clsx', 'tailwind-merge'];
1392
- const missingBaseDeps = baseDeps.filter(dep => !checkPackageInstalled(dep));
1839
+
1840
+ // Step 2: Install dependencies
1841
+ info(' → Installing dependencies');
1842
+ const missingBaseDeps = BASE_DEPENDENCIES.filter(dep => !checkPackageInstalled(dep));
1393
1843
 
1394
1844
  if (missingBaseDeps.length > 0) {
1395
1845
  await installPackages(missingBaseDeps);
1396
- } else {
1397
- info('Base dependencies already installed.');
1398
1846
  }
1399
-
1400
- // Install tw-animate-css
1401
- if (!checkPackageInstalled('tw-animate-css')) {
1402
- info('Installing tw-animate-css for animations...');
1403
- await installPackages(['tw-animate-css']);
1847
+
1848
+ // Install Next.js dependencies if not present
1849
+ const nextDeps = [
1850
+ "next", "react", "react-dom", "typescript",
1851
+ "@types/node", "@types/react", "@types/react-dom",
1852
+ "tailwindcss", "autoprefixer", "postcss",
1853
+ "tailwindcss-animate", "eslint", "eslint-config-next"
1854
+ ];
1855
+
1856
+ const missingNextDeps = nextDeps.filter(dep => !checkPackageInstalled(dep));
1857
+ if (missingNextDeps.length > 0) {
1858
+ await installPackages(missingNextDeps);
1404
1859
  }
1405
-
1406
- // Install CSS styles
1407
- info('\nInstalling CSS styles...');
1408
- await installCSSStyles(config, DEFAULT_REGISTRY_URL, false, false);
1409
-
1410
- success('\nāœ… Project initialized successfully!');
1411
- info('\nYou can now add components using:');
1860
+
1861
+ // Install tw-animate-css for animations
1862
+ if (!checkPackageInstalled(PACKAGE_NAMES.TW_ANIMATE_CSS)) {
1863
+ await installPackages([PACKAGE_NAMES.TW_ANIMATE_CSS]);
1864
+ }
1865
+
1866
+ // Step 3: Create utils.ts file
1867
+ info(' → Create 1 files:');
1868
+ const utilsPath = path.join(process.cwd(), 'lib', 'utils.ts');
1869
+ createUtilsFile(utilsPath);
1870
+ info(' → libs/utils.ts');
1871
+
1872
+ success('\nāœ… Success! Successfully initialization.');
1873
+ info('\nYou could add the components now.');
1874
+ info('\nAvailable commands:');
1412
1875
  info(' npx weloop-kosign@latest add <component-name>');
1413
1876
  info(' pnpm dlx weloop-kosign@latest add <component-name>');
1414
1877
  info(' yarn weloop-kosign@latest add <component-name>\n');
@@ -1427,7 +1890,7 @@ async function initProject() {
1427
1890
  async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
1428
1891
  try {
1429
1892
  const index = await loadIndex(registryUrl);
1430
-
1893
+
1431
1894
  console.log('\nAvailable components:\n');
1432
1895
  index.registry.forEach(comp => {
1433
1896
  // Show dependencies if component has any
@@ -1463,13 +1926,13 @@ async function main() {
1463
1926
  const args = process.argv.slice(2);
1464
1927
  const command = args[0];
1465
1928
  const componentName = args[1];
1466
-
1929
+
1467
1930
  // Parse --registry option if provided
1468
1931
  const registryIndex = args.indexOf('--registry');
1469
1932
  const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
1470
1933
  ? args[registryIndex + 1]
1471
1934
  : DEFAULT_REGISTRY_URL;
1472
-
1935
+
1473
1936
  // Build options object
1474
1937
  const options = {
1475
1938
  overwrite: args.includes('--overwrite') || args.includes('-f'),
@@ -1505,7 +1968,7 @@ Examples:
1505
1968
  case 'init':
1506
1969
  await initProject();
1507
1970
  break;
1508
-
1971
+
1509
1972
  case 'add':
1510
1973
  if (!componentName) {
1511
1974
  error('Please provide a component name');
@@ -1514,18 +1977,18 @@ Examples:
1514
1977
  }
1515
1978
  await installComponent(componentName, options);
1516
1979
  break;
1517
-
1980
+
1518
1981
  case 'list':
1519
1982
  await listComponents(registryUrl);
1520
1983
  break;
1521
-
1984
+
1522
1985
  case 'css':
1523
1986
  case 'styles':
1524
1987
  // Both 'css' and 'styles' commands do the same thing
1525
1988
  const config = loadComponentsConfig();
1526
1989
  await installCSSStyles(config, registryUrl, options.overwrite, false);
1527
1990
  break;
1528
-
1991
+
1529
1992
  default:
1530
1993
  error(`Unknown command: ${command}`);
1531
1994
  console.log('Available commands: init, add, list, css');