weloop-kosign 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/cli-remote.js +543 -217
@@ -8,16 +8,19 @@
8
8
  * - If running in external projects: uses remote GitLab/GitHub URL
9
9
  *
10
10
  * Usage (via npx - recommended):
11
+ * npx weloop-kosign@latest init
11
12
  * npx weloop-kosign@latest add <component-name>
12
13
  * npx weloop-kosign@latest list
13
14
  * npx weloop-kosign@latest css [--overwrite]
14
15
  *
15
16
  * Usage (local development):
17
+ * node scripts/cli-remote.js init [--registry <url|path>]
16
18
  * node scripts/cli-remote.js add <component-name> [--registry <url|path>]
17
19
  * node scripts/cli-remote.js list [--registry <url|path>]
18
20
  * node scripts/cli-remote.js css [--registry <url|path>] [--overwrite]
19
21
  *
20
22
  * Examples:
23
+ * npx weloop-kosign@latest init
21
24
  * npx weloop-kosign@latest add button
22
25
  * npx weloop-kosign@latest add button --registry ./registry
23
26
  * npx weloop-kosign@latest css
@@ -45,22 +48,81 @@ const { execSync } = require('child_process');
45
48
  */
46
49
  function getDefaultRegistryUrl() {
47
50
  const localRegistryPath = path.join(__dirname, '../registry');
48
-
51
+
49
52
  // Check if we're running in the component library project itself
50
53
  if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
51
54
  return localRegistryPath;
52
55
  }
53
-
56
+
54
57
  // Fall back to environment variable or default remote URL
55
- return process.env.WELOOP_REGISTRY_URL ||
58
+ return process.env.WELOOP_REGISTRY_URL ||
56
59
  'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
57
60
  }
58
61
 
59
62
  const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
60
63
 
64
+ // ============================================================================
65
+ // CONSTANTS
66
+ // ============================================================================
67
+
61
68
  // Network timeout constants (in milliseconds)
62
69
  const REQUEST_TIMEOUT = 15000;
63
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
+ ];
64
126
 
65
127
  // ============================================================================
66
128
  // UTILITIES - Logging and File System Helpers
@@ -127,6 +189,38 @@ function ensureDirectoryExists(dirPath) {
127
189
  }
128
190
  }
129
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
+
130
224
  /**
131
225
  * Checks if a path is a local file path or a remote URL
132
226
  * @param {string} pathOrUrl - Path or URL to check
@@ -155,12 +249,12 @@ function detectPackageManager() {
155
249
  if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
156
250
  if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
157
251
  if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
158
-
252
+
159
253
  // Fallback: check npm user agent (set by npm/yarn/pnpm when running)
160
254
  const userAgent = process.env.npm_config_user_agent || '';
161
255
  if (userAgent.includes('yarn')) return 'yarn';
162
256
  if (userAgent.includes('pnpm')) return 'pnpm';
163
-
257
+
164
258
  // Default to npm if we can't detect anything
165
259
  return 'npm';
166
260
  }
@@ -188,9 +282,9 @@ function getInstallCommand(packageManager, packages) {
188
282
  * @returns {boolean} True if package is installed, false otherwise
189
283
  */
190
284
  function checkPackageInstalled(packageName) {
191
- const packageJsonPath = path.join(process.cwd(), 'package.json');
285
+ const packageJsonPath = path.join(process.cwd(), PATHS.PACKAGE_JSON);
192
286
  if (!fs.existsSync(packageJsonPath)) return false;
193
-
287
+
194
288
  try {
195
289
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
196
290
  // Check both dependencies and devDependencies
@@ -209,16 +303,16 @@ function checkPackageInstalled(packageName) {
209
303
  * @returns {string[]} List of packages that need to be installed
210
304
  */
211
305
  function getMissingDependencies(requiredDeps) {
212
- const packageJsonPath = path.join(process.cwd(), 'package.json');
306
+ const packageJsonPath = path.join(process.cwd(), PATHS.PACKAGE_JSON);
213
307
  if (!fs.existsSync(packageJsonPath)) {
214
308
  // No package.json means we need to install everything
215
309
  return requiredDeps;
216
310
  }
217
-
311
+
218
312
  try {
219
313
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
220
314
  const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
221
-
315
+
222
316
  return requiredDeps.filter(dep => {
223
317
  // For scoped packages like @radix-ui/react-button, also check @radix-ui
224
318
  const depName = dep.split('/').slice(0, 2).join('/');
@@ -240,44 +334,44 @@ function getMissingDependencies(requiredDeps) {
240
334
  */
241
335
  async function installPackages(packages) {
242
336
  if (packages.length === 0) return;
243
-
337
+
244
338
  const packageManager = detectPackageManager();
245
339
  info(`\nInstalling dependencies: ${packages.join(', ')}`);
246
-
340
+
247
341
  try {
248
342
  const installCmd = getInstallCommand(packageManager, packages);
249
-
343
+
250
344
  // npm has noisy ERESOLVE warnings that aren't real errors
251
345
  // We filter these out while keeping actual error messages
252
346
  if (packageManager === 'npm') {
253
347
  const { spawn } = require('child_process');
254
348
  const [cmd, ...args] = installCmd.split(' ');
255
-
349
+
256
350
  await new Promise((resolve, reject) => {
257
351
  const proc = spawn(cmd, args, {
258
352
  cwd: process.cwd(),
259
353
  stdio: ['inherit', 'inherit', 'pipe'],
260
354
  shell: true
261
355
  });
262
-
356
+
263
357
  let stderr = '';
264
358
  proc.stderr.on('data', (data) => {
265
359
  const output = data.toString();
266
360
  // Filter out ERESOLVE peer dependency warnings (these are usually safe to ignore)
267
361
  // but keep actual error messages visible to the user
268
- if (!output.includes('ERESOLVE overriding peer dependency') &&
269
- !output.includes('npm warn ERESOLVE')) {
362
+ if (!output.includes('ERESOLVE overriding peer dependency') &&
363
+ !output.includes('npm warn ERESOLVE')) {
270
364
  process.stderr.write(data);
271
365
  }
272
366
  stderr += output;
273
367
  });
274
-
368
+
275
369
  proc.on('close', (code) => {
276
370
  if (code !== 0) {
277
371
  // npm sometimes exits with non-zero code due to warnings, not real errors
278
372
  // Check if there's an actual error message (not just ERESOLVE warnings)
279
- const hasRealError = stderr.includes('npm error') &&
280
- !stderr.match(/npm error.*ERESOLVE/);
373
+ const hasRealError = stderr.includes('npm error') &&
374
+ !stderr.match(/npm error.*ERESOLVE/);
281
375
  if (hasRealError) {
282
376
  reject(new Error(`Installation failed with code ${code}`));
283
377
  } else {
@@ -288,13 +382,13 @@ async function installPackages(packages) {
288
382
  resolve();
289
383
  }
290
384
  });
291
-
385
+
292
386
  proc.on('error', reject);
293
387
  });
294
388
  } else {
295
389
  execSync(installCmd, { stdio: 'inherit', cwd: process.cwd() });
296
390
  }
297
-
391
+
298
392
  success(`Dependencies installed successfully`);
299
393
  } catch (error) {
300
394
  warn(`Failed to install dependencies automatically`);
@@ -316,25 +410,25 @@ async function installPackages(packages) {
316
410
  * @returns {object} The default configuration object
317
411
  */
318
412
  function createDefaultComponentsConfig() {
319
- const configPath = path.join(process.cwd(), 'components.json');
320
-
413
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
414
+
321
415
  // Detect Next.js project structure
322
416
  // App Router uses 'app/globals.css', Pages Router uses 'styles/globals.css'
323
417
  const hasAppDir = fs.existsSync(path.join(process.cwd(), 'app'));
324
- const cssPath = hasAppDir ? 'app/globals.css' : 'styles/globals.css';
325
-
418
+ const cssPath = hasAppDir ? PATHS.APP_GLOBALS_CSS : PATHS.STYLES_GLOBALS_CSS;
419
+
326
420
  const defaultConfig = {
327
- style: 'blue',
421
+ style: DEFAULTS.STYLE,
328
422
  rsc: true,
329
423
  tsx: true,
330
424
  tailwind: {
331
425
  config: 'tailwind.config.ts',
332
426
  css: cssPath,
333
- baseColor: 'neutral',
427
+ baseColor: DEFAULTS.BASE_COLOR,
334
428
  cssVariables: true,
335
429
  prefix: ''
336
430
  },
337
- iconLibrary: 'lucide',
431
+ iconLibrary: DEFAULTS.ICON_LIBRARY,
338
432
  aliases: {
339
433
  components: '@/components',
340
434
  utils: '@/lib/utils',
@@ -342,13 +436,13 @@ function createDefaultComponentsConfig() {
342
436
  lib: '@/lib',
343
437
  hooks: '@/hooks'
344
438
  },
345
- registry: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry/index.json'
439
+ registry: REGISTRY_URLS.INDEX
346
440
  };
347
-
441
+
348
442
  fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
349
- success(`Created components.json`);
443
+ success(`Created ${PATHS.COMPONENTS_JSON}`);
350
444
  info(` You can customize this file to match your project setup`);
351
-
445
+
352
446
  return defaultConfig;
353
447
  }
354
448
 
@@ -359,15 +453,20 @@ function createDefaultComponentsConfig() {
359
453
  * @returns {object} The configuration object
360
454
  */
361
455
  function loadComponentsConfig() {
362
- const configPath = path.join(process.cwd(), 'components.json');
363
-
456
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
457
+
364
458
  if (!fs.existsSync(configPath)) {
365
- info('components.json not found. Creating default configuration...');
459
+ info(`${PATHS.COMPONENTS_JSON} not found. Creating default configuration...`);
366
460
  return createDefaultComponentsConfig();
367
461
  }
368
-
369
- const content = fs.readFileSync(configPath, 'utf-8');
370
- 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
+ }
371
470
  }
372
471
 
373
472
  /**
@@ -383,21 +482,21 @@ function loadComponentsConfig() {
383
482
  * @param {number} retries - Number of retry attempts (default: 3)
384
483
  * @returns {Promise<object>} Parsed JSON object
385
484
  */
386
- async function fetchJSON(urlOrPath, retries = 3) {
485
+ async function fetchJSON(urlOrPath, retries = MAX_RETRIES) {
387
486
  // Handle local file paths
388
487
  if (isLocalPath(urlOrPath)) {
389
488
  return new Promise((resolve, reject) => {
390
489
  try {
391
490
  // Resolve relative paths to absolute
392
- const fullPath = path.isAbsolute(urlOrPath)
393
- ? urlOrPath
491
+ const fullPath = path.isAbsolute(urlOrPath)
492
+ ? urlOrPath
394
493
  : path.join(process.cwd(), urlOrPath);
395
-
494
+
396
495
  if (!fs.existsSync(fullPath)) {
397
496
  reject(new Error(`File not found: ${fullPath}`));
398
497
  return;
399
498
  }
400
-
499
+
401
500
  const content = fs.readFileSync(fullPath, 'utf-8');
402
501
  resolve(JSON.parse(content));
403
502
  } catch (e) {
@@ -405,38 +504,38 @@ async function fetchJSON(urlOrPath, retries = 3) {
405
504
  }
406
505
  });
407
506
  }
408
-
507
+
409
508
  // Handle remote URLs
410
509
  return new Promise((resolve, reject) => {
411
510
  const client = urlOrPath.startsWith('https') ? https : http;
412
511
  let timeout;
413
512
  let request;
414
-
513
+
415
514
  const makeRequest = () => {
416
515
  request = client.get(urlOrPath, (res) => {
417
516
  clearTimeout(timeout);
418
-
517
+
419
518
  // Handle HTTP redirects (301, 302)
420
519
  if (res.statusCode === 302 || res.statusCode === 301) {
421
520
  return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
422
521
  }
423
-
522
+
424
523
  // Provide helpful error messages for common HTTP status codes
425
524
  if (res.statusCode === 403) {
426
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.`));
427
526
  return;
428
527
  }
429
-
528
+
430
529
  if (res.statusCode === 404) {
431
530
  reject(new Error(`Not found (404). Check that the registry files are pushed to the repository and the URL is correct.`));
432
531
  return;
433
532
  }
434
-
533
+
435
534
  if (res.statusCode !== 200) {
436
535
  reject(new Error(`Failed to fetch: HTTP ${res.statusCode} - ${res.statusMessage || 'Unknown error'}`));
437
536
  return;
438
537
  }
439
-
538
+
440
539
  // Collect response data
441
540
  let data = '';
442
541
  res.on('data', (chunk) => { data += chunk; });
@@ -453,7 +552,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
453
552
  }
454
553
  });
455
554
  });
456
-
555
+
457
556
  // Handle network errors with automatic retry
458
557
  request.on('error', (err) => {
459
558
  clearTimeout(timeout);
@@ -466,7 +565,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
466
565
  reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
467
566
  }
468
567
  });
469
-
568
+
470
569
  // Set request timeout with retry logic
471
570
  timeout = setTimeout(() => {
472
571
  request.destroy();
@@ -480,7 +579,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
480
579
  }
481
580
  }, REQUEST_TIMEOUT);
482
581
  };
483
-
582
+
484
583
  makeRequest();
485
584
  });
486
585
  }
@@ -500,34 +599,34 @@ async function fetchText(urlOrPath) {
500
599
  }
501
600
  return fs.readFileSync(urlOrPath, 'utf-8');
502
601
  }
503
-
602
+
504
603
  // Handle remote URLs
505
604
  return new Promise((resolve, reject) => {
506
605
  const client = urlOrPath.startsWith('https') ? https : http;
507
606
  let data = '';
508
-
607
+
509
608
  const req = client.get(urlOrPath, (res) => {
510
609
  // Follow redirects
511
610
  if (res.statusCode === 302 || res.statusCode === 301) {
512
611
  return fetchText(res.headers.location).then(resolve).catch(reject);
513
612
  }
514
-
613
+
515
614
  // Handle common HTTP errors
516
615
  if (res.statusCode === 403) {
517
616
  reject(new Error('Access forbidden - repository may be private'));
518
617
  return;
519
618
  }
520
-
619
+
521
620
  if (res.statusCode === 404) {
522
621
  reject(new Error('CSS file not found in repository'));
523
622
  return;
524
623
  }
525
-
624
+
526
625
  if (res.statusCode !== 200) {
527
626
  reject(new Error(`HTTP ${res.statusCode}`));
528
627
  return;
529
628
  }
530
-
629
+
531
630
  // Collect response data
532
631
  res.on('data', (chunk) => { data += chunk; });
533
632
  res.on('end', () => {
@@ -539,7 +638,7 @@ async function fetchText(urlOrPath) {
539
638
  resolve(data);
540
639
  });
541
640
  });
542
-
641
+
543
642
  // Handle network errors with automatic retry
544
643
  req.on('error', (err) => {
545
644
  if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
@@ -550,7 +649,7 @@ async function fetchText(urlOrPath) {
550
649
  reject(err);
551
650
  }
552
651
  });
553
-
652
+
554
653
  // Set request timeout
555
654
  req.setTimeout(REQUEST_TIMEOUT, () => {
556
655
  req.destroy();
@@ -570,13 +669,14 @@ async function fetchText(urlOrPath) {
570
669
  function resolveRegistryPath(baseUrl, fileName) {
571
670
  if (isLocalPath(baseUrl)) {
572
671
  // Resolve local paths (absolute or relative)
573
- const basePath = path.isAbsolute(baseUrl)
574
- ? baseUrl
672
+ const basePath = path.isAbsolute(baseUrl)
673
+ ? baseUrl
575
674
  : path.join(process.cwd(), baseUrl);
576
675
  return path.join(basePath, fileName);
577
676
  }
578
- // For remote URLs, just append the filename
579
- return `${baseUrl}/${fileName}`;
677
+ // For remote URLs, append filename with proper separator
678
+ const separator = baseUrl.endsWith('/') ? '' : '/';
679
+ return `${baseUrl}${separator}${fileName}`;
580
680
  }
581
681
 
582
682
  /**
@@ -593,6 +693,7 @@ async function loadRegistry(componentName, registryBaseUrl) {
593
693
  return await fetchJSON(registryPath);
594
694
  } catch (err) {
595
695
  // Component not found - return null instead of throwing
696
+ // This allows the caller to handle missing components gracefully
596
697
  return null;
597
698
  }
598
699
  }
@@ -605,7 +706,7 @@ async function loadRegistry(componentName, registryBaseUrl) {
605
706
  * @returns {Promise<object>} Index object containing list of components
606
707
  */
607
708
  async function loadIndex(registryBaseUrl) {
608
- const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
709
+ const indexPath = resolveRegistryPath(registryBaseUrl, PATHS.REGISTRY_INDEX);
609
710
  try {
610
711
  return await fetchJSON(indexPath);
611
712
  } catch (err) {
@@ -626,12 +727,18 @@ async function loadIndex(registryBaseUrl) {
626
727
  * @returns {boolean} True if Weloop styles are present
627
728
  */
628
729
  function hasWeloopStyles(content) {
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
634
- 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');
635
742
  }
636
743
 
637
744
  /**
@@ -642,16 +749,15 @@ function hasWeloopStyles(content) {
642
749
  * @returns {string} CSS content with duplicates removed
643
750
  */
644
751
  function removeDuplicateTwAnimateImports(cssContent) {
645
- const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
646
- const matches = cssContent.match(twAnimatePattern);
647
-
752
+ const matches = cssContent.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
753
+
648
754
  if (matches && matches.length > 1) {
649
755
  // Remove all occurrences first
650
- let cleaned = cssContent.replace(twAnimatePattern, '');
756
+ let cleaned = cssContent.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
651
757
  // Add it back once, right after tailwindcss import if it exists
652
- if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
758
+ if (hasTailwindImport(cleaned)) {
653
759
  cleaned = cleaned.replace(
654
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
760
+ CSS_PATTERNS.TAILWIND_IMPORT,
655
761
  '$1@import "tw-animate-css";\n'
656
762
  );
657
763
  } else {
@@ -660,7 +766,7 @@ function removeDuplicateTwAnimateImports(cssContent) {
660
766
  }
661
767
  return cleaned;
662
768
  }
663
-
769
+
664
770
  return cssContent;
665
771
  }
666
772
 
@@ -672,12 +778,11 @@ function removeDuplicateTwAnimateImports(cssContent) {
672
778
  * @returns {string} CSS content with duplicates removed
673
779
  */
674
780
  function removeDuplicateCustomVariant(cssContent) {
675
- const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
676
- const matches = cssContent.match(variantPattern);
781
+ const matches = cssContent.match(CSS_PATTERNS.CUSTOM_VARIANT);
677
782
 
678
783
  if (matches && matches.length > 1) {
679
784
  // Remove all occurrences and add back one at the top
680
- const withoutVariants = cssContent.replace(variantPattern, '');
785
+ const withoutVariants = cssContent.replace(CSS_PATTERNS.CUSTOM_VARIANT, '');
681
786
  return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
682
787
  }
683
788
 
@@ -700,30 +805,28 @@ function removeDuplicateCustomVariant(cssContent) {
700
805
  */
701
806
  function normalizeCSSFormat(cssContent) {
702
807
  // Extract @custom-variant declaration
703
- const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
704
- const variantMatch = cssContent.match(variantPattern);
808
+ const variantMatch = cssContent.match(CSS_PATTERNS.CUSTOM_VARIANT);
705
809
  const hasVariant = variantMatch && variantMatch.length > 0;
706
-
810
+
707
811
  // Extract all @import statements
708
- const importPattern = /@import\s+["'][^"']+["'];?\s*\n?/g;
709
- const imports = cssContent.match(importPattern) || [];
710
-
812
+ const imports = cssContent.match(CSS_PATTERNS.IMPORT_STATEMENT) || [];
813
+
711
814
  // Separate imports by type for proper ordering
712
815
  const tailwindImport = imports.find(imp => imp.includes('tailwindcss'));
713
816
  const twAnimateImport = imports.find(imp => imp.includes('tw-animate-css'));
714
- const otherImports = imports.filter(imp =>
817
+ const otherImports = imports.filter(imp =>
715
818
  !imp.includes('tailwindcss') && !imp.includes('tw-animate-css')
716
819
  );
717
-
820
+
718
821
  // Remove all imports and variant from content to get the rest
719
822
  let content = cssContent
720
- .replace(variantPattern, '')
721
- .replace(importPattern, '')
823
+ .replace(CSS_PATTERNS.CUSTOM_VARIANT, '')
824
+ .replace(CSS_PATTERNS.IMPORT_STATEMENT, '')
722
825
  .trim();
723
-
826
+
724
827
  // Build normalized format in the correct order
725
828
  let normalized = '';
726
-
829
+
727
830
  // Step 1: Imports first (tailwindcss, then tw-animate-css, then others)
728
831
  if (tailwindImport) {
729
832
  normalized += tailwindImport.trim() + '\n';
@@ -734,23 +837,19 @@ function normalizeCSSFormat(cssContent) {
734
837
  if (otherImports.length > 0) {
735
838
  normalized += otherImports.join('') + '\n';
736
839
  }
737
-
840
+
738
841
  // Step 2: @custom-variant second (if it exists)
739
842
  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
- }
843
+ normalized += normalized ? '\n@custom-variant dark (&:is(.dark *));\n' : '@custom-variant dark (&:is(.dark *));\n';
745
844
  }
746
-
845
+
747
846
  // Step 3: Rest of content (theme variables, etc.)
748
847
  if (normalized && content) {
749
848
  normalized += '\n' + content;
750
849
  } else if (content) {
751
850
  normalized = content;
752
851
  }
753
-
852
+
754
853
  return normalized.trim() + (normalized ? '\n' : '');
755
854
  }
756
855
 
@@ -766,32 +865,32 @@ function normalizeCSSFormat(cssContent) {
766
865
  * @returns {string} Processed CSS content
767
866
  */
768
867
  function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
769
- const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
770
868
  let processed = cssContent;
771
-
869
+
772
870
  if (!hasTwAnimate) {
773
871
  // Package not installed - remove import to prevent build errors
774
- processed = cssContent.replace(twAnimatePattern, '');
872
+ processed = cssContent.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
775
873
  if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
874
+ const installCmd = `npm install ${PACKAGE_NAMES.TW_ANIMATE_CSS}`;
776
875
  if (forceUpdate) {
777
- warn('tw-animate-css package not found - removed from CSS to prevent build errors');
778
- info(' Install with: npm install tw-animate-css');
779
- 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');
780
879
  } else {
781
- warn('tw-animate-css package not found - removed from CSS');
782
- 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}`);
783
882
  info(' Or add the import manually after installing the package');
784
883
  }
785
884
  }
786
885
  } else {
787
886
  // Package is installed - ensure import exists and remove duplicates
788
887
  processed = removeDuplicateTwAnimateImports(processed);
789
-
888
+
790
889
  // Add import if it doesn't exist (right after tailwindcss if present)
791
- if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
792
- if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
890
+ if (!processed.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
891
+ if (hasTailwindImport(processed)) {
793
892
  processed = processed.replace(
794
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
893
+ CSS_PATTERNS.TAILWIND_IMPORT,
795
894
  '$1@import "tw-animate-css";\n'
796
895
  );
797
896
  } else {
@@ -799,7 +898,7 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
799
898
  }
800
899
  }
801
900
  }
802
-
901
+
803
902
  return processed;
804
903
  }
805
904
 
@@ -811,7 +910,7 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
811
910
  * @returns {string} CSS content without tailwindcss imports
812
911
  */
813
912
  function removeTailwindImport(cssContent) {
814
- return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
913
+ return cssContent.replace(CSS_PATTERNS.TAILWIND_IMPORT, '');
815
914
  }
816
915
 
817
916
  /**
@@ -826,23 +925,23 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
826
925
  if (!hasTwAnimate) {
827
926
  return cssContent;
828
927
  }
829
-
928
+
830
929
  // Remove duplicates first
831
930
  let cleaned = removeDuplicateTwAnimateImports(cssContent);
832
-
931
+
833
932
  // Check if import already exists
834
- if (cleaned.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
933
+ if (cleaned.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
835
934
  return cleaned;
836
935
  }
837
-
936
+
838
937
  // Add import after tailwindcss if it exists, otherwise at the beginning
839
- if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
938
+ if (hasTailwindImport(cleaned)) {
840
939
  return cleaned.replace(
841
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
940
+ CSS_PATTERNS.TAILWIND_IMPORT,
842
941
  '$1@import "tw-animate-css";\n'
843
942
  );
844
943
  }
845
-
944
+
846
945
  return '@import "tw-animate-css";\n' + cleaned;
847
946
  }
848
947
 
@@ -860,35 +959,32 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
860
959
  * @returns {string} Merged CSS content
861
960
  */
862
961
  function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
863
- const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
962
+ const tailwindMatch = existing.match(CSS_PATTERNS.TAILWIND_IMPORT);
864
963
  if (!tailwindMatch) {
865
964
  // No tailwindcss import found - just prepend Weloop styles
866
965
  return weloopStyles + '\n\n' + existing;
867
966
  }
868
-
967
+
869
968
  // Split existing CSS around the tailwindcss import
870
- const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
871
- const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
872
-
969
+ const tailwindIndex = existing.indexOf(tailwindMatch[0]);
970
+ const beforeTailwind = existing.substring(0, tailwindIndex);
971
+ const afterTailwind = existing.substring(tailwindIndex + tailwindMatch[0].length);
972
+
873
973
  // Check if tw-animate-css import already exists
874
- const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
875
- const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
876
-
974
+ const hasTwAnimateInExisting = existing.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
975
+ const hasTwAnimateInWeloop = weloopStyles.match(CSS_PATTERNS.TW_ANIMATE_IMPORT);
976
+
877
977
  // If existing file already has tw-animate import, remove it from Weloop styles
878
978
  // to avoid duplicating it in the merge output
879
979
  let weloopStylesCleaned = weloopStyles;
880
980
  if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
881
- weloopStylesCleaned = weloopStyles.replace(
882
- /@import\s+["']tw-animate-css["'];?\s*\n?/g,
883
- ''
884
- );
981
+ weloopStylesCleaned = weloopStyles.replace(CSS_PATTERNS.TW_ANIMATE_IMPORT, '');
885
982
  }
886
-
983
+
887
984
  // Add tw-animate import if needed (package installed but import missing)
888
- let importsToAdd = '';
889
- if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
890
- importsToAdd = '@import "tw-animate-css";\n';
891
- }
985
+ const importsToAdd = (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop)
986
+ ? '@import "tw-animate-css";\n'
987
+ : '';
892
988
 
893
989
  // Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
894
990
  return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
@@ -907,28 +1003,26 @@ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
907
1003
  * @returns {string} CSS content with Weloop styles replaced
908
1004
  */
909
1005
  function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
910
- const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
911
- const imports = existing.match(importPattern) || [];
1006
+ const imports = existing.match(CSS_PATTERNS.IMPORT_STATEMENT) || [];
912
1007
  const importsText = imports.join('');
913
-
1008
+
914
1009
  // Find where Weloop styles start (look for @theme inline or :root)
915
- const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
916
- const weloopStartMatch = existing.search(weloopStartPattern);
917
-
1010
+ const weloopStartMatch = existing.search(CSS_PATTERNS.WELOOP_START);
1011
+
918
1012
  if (weloopStartMatch === -1) {
919
1013
  // No existing Weloop styles found - just append
920
1014
  return existing + '\n\n' + newStyles;
921
1015
  }
922
-
1016
+
923
1017
  // Extract content before Weloop styles
924
1018
  let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
925
1019
  // Keep non-Weloop imports (filter out tw-animate if package not installed)
926
- const nonWeloopImports = importsText.split('\n').filter(imp =>
1020
+ const nonWeloopImports = importsText.split('\n').filter(imp =>
927
1021
  !imp.includes('tw-animate-css') || hasTwAnimate
928
1022
  ).join('\n');
929
1023
  // Remove all imports from contentBeforeWeloop (we'll add them back)
930
- contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
931
-
1024
+ contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(CSS_PATTERNS.IMPORT_STATEMENT, '');
1025
+
932
1026
  // Return: preserved imports + content before Weloop + new Weloop styles
933
1027
  return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
934
1028
  }
@@ -944,11 +1038,11 @@ function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
944
1038
  */
945
1039
  async function fetchCSSFromRegistry(registryUrl) {
946
1040
  let sourceCssPath;
947
-
1041
+
948
1042
  if (isLocalPath(registryUrl)) {
949
1043
  // Local path: find app/globals.css relative to registry directory
950
- const basePath = path.isAbsolute(registryUrl)
951
- ? path.dirname(registryUrl)
1044
+ const basePath = path.isAbsolute(registryUrl)
1045
+ ? path.dirname(registryUrl)
952
1046
  : path.join(process.cwd(), path.dirname(registryUrl));
953
1047
  sourceCssPath = path.join(basePath, 'app', 'globals.css');
954
1048
  } else {
@@ -956,7 +1050,7 @@ async function fetchCSSFromRegistry(registryUrl) {
956
1050
  const baseUrl = registryUrl.replace('/registry', '');
957
1051
  sourceCssPath = `${baseUrl}/app/globals.css`;
958
1052
  }
959
-
1053
+
960
1054
  return await fetchText(sourceCssPath);
961
1055
  }
962
1056
 
@@ -974,52 +1068,49 @@ async function fetchCSSFromRegistry(registryUrl) {
974
1068
  * @param {boolean} silent - Whether to suppress output messages
975
1069
  */
976
1070
  async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
977
- const cssPath = config.tailwind?.css || 'app/globals.css';
1071
+ const cssPath = config.tailwind?.css || DEFAULTS.CSS_PATH;
978
1072
  const fullCssPath = path.join(process.cwd(), cssPath);
979
-
1073
+
980
1074
  // Check if Weloop styles already exist in the file
981
1075
  let hasWeloopStylesInFile = false;
982
1076
  if (fs.existsSync(fullCssPath)) {
983
1077
  const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
984
1078
  hasWeloopStylesInFile = hasWeloopStyles(existingContent);
985
-
1079
+
986
1080
  // If styles already exist and we're not forcing update, skip silently
987
1081
  // This prevents unnecessary updates when installing components
988
1082
  if (hasWeloopStylesInFile && !forceUpdate && silent) {
989
1083
  return;
990
1084
  }
991
1085
  }
992
-
1086
+
993
1087
  try {
994
1088
  if (!silent) {
995
1089
  info('Installing CSS styles...');
996
1090
  }
997
-
1091
+
998
1092
  const cssContent = await fetchCSSFromRegistry(registryUrl);
999
1093
  ensureDirectoryExists(path.dirname(fullCssPath));
1000
-
1001
- const hasTwAnimate = checkPackageInstalled('tw-animate-css');
1094
+
1095
+ const hasTwAnimate = checkPackageInstalled(PACKAGE_NAMES.TW_ANIMATE_CSS);
1002
1096
  let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
1003
-
1097
+
1004
1098
  // Handle file installation based on mode and existing file state
1005
1099
  if (forceUpdate && fs.existsSync(fullCssPath)) {
1006
1100
  // Mode 1: --overwrite flag - Replace entire file with fresh styles
1007
- let normalized = normalizeCSSFormat(processedCssContent);
1008
- normalized = removeDuplicateTwAnimateImports(normalized);
1009
- normalized = removeDuplicateCustomVariant(normalized);
1101
+ const normalized = normalizeAndCleanCSS(processedCssContent, hasTwAnimate);
1010
1102
  fs.writeFileSync(fullCssPath, normalized);
1011
1103
  if (!silent) {
1012
1104
  success(`Overwritten ${cssPath} with Weloop styles`);
1013
1105
  if (hasTwAnimate) {
1014
- info(` tw-animate-css import included`);
1106
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1015
1107
  }
1016
1108
  }
1017
1109
  } else if (fs.existsSync(fullCssPath)) {
1018
1110
  // Mode 2: Normal update - Intelligently merge with existing styles
1019
1111
  const existing = fs.readFileSync(fullCssPath, 'utf-8');
1020
- const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
1021
- existing.includes('@tailwind base');
1022
-
1112
+ const existingHasTailwind = hasTailwindImport(existing);
1113
+
1023
1114
  if (hasWeloopStylesInFile) {
1024
1115
  // Case 2a: Weloop styles already exist - replace them with updated versions
1025
1116
  let weloopStyles = removeTailwindImport(processedCssContent);
@@ -1028,7 +1119,7 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1028
1119
  finalContent = removeDuplicateTwAnimateImports(finalContent);
1029
1120
  finalContent = removeDuplicateCustomVariant(finalContent);
1030
1121
  finalContent = normalizeCSSFormat(finalContent);
1031
-
1122
+
1032
1123
  // Only write if content actually changed (prevents unnecessary file updates)
1033
1124
  if (finalContent !== existing) {
1034
1125
  fs.writeFileSync(fullCssPath, finalContent);
@@ -1037,31 +1128,29 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1037
1128
  }
1038
1129
  }
1039
1130
  // If no changes, silently skip (file is already up to date)
1040
- } else if (hasTailwindImport) {
1131
+ } else if (existingHasTailwind) {
1041
1132
  // Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
1042
1133
  let weloopStyles = removeTailwindImport(processedCssContent);
1043
1134
  weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
1044
1135
  let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
1045
- merged = removeDuplicateTwAnimateImports(merged);
1046
- merged = removeDuplicateCustomVariant(merged);
1047
- merged = normalizeCSSFormat(merged);
1136
+ merged = normalizeAndCleanCSS(merged, hasTwAnimate);
1048
1137
  fs.writeFileSync(fullCssPath, merged);
1049
1138
  if (!silent) {
1050
1139
  success(`Updated ${cssPath} with Weloop styles`);
1051
1140
  if (hasTwAnimate) {
1052
- info(` tw-animate-css import included`);
1141
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1053
1142
  }
1054
1143
  info(` Your existing styles are preserved`);
1055
1144
  }
1056
1145
  } else {
1057
1146
  // Case 2c: No Tailwind imports - prepend Weloop styles to existing content
1058
1147
  let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
1059
-
1148
+
1060
1149
  // Ensure tw-animate import exists if package is installed
1061
- if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
1062
- if (finalCssContent.includes('@import "tailwindcss"')) {
1150
+ if (hasTwAnimate && !finalCssContent.match(CSS_PATTERNS.TW_ANIMATE_IMPORT)) {
1151
+ if (hasTailwindImport(finalCssContent)) {
1063
1152
  finalCssContent = finalCssContent.replace(
1064
- /(@import\s+["']tailwindcss["'];?\s*\n?)/,
1153
+ CSS_PATTERNS.TAILWIND_IMPORT,
1065
1154
  '$1@import "tw-animate-css";\n'
1066
1155
  );
1067
1156
  } else {
@@ -1069,35 +1158,34 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1069
1158
  }
1070
1159
  }
1071
1160
  let combined = finalCssContent + '\n\n' + existing;
1072
- combined = normalizeCSSFormat(combined);
1161
+ combined = normalizeAndCleanCSS(combined, hasTwAnimate);
1073
1162
  fs.writeFileSync(fullCssPath, combined);
1074
1163
  if (!silent) {
1075
1164
  success(`Updated ${cssPath} with Weloop styles`);
1076
1165
  if (hasTwAnimate) {
1077
- info(` tw-animate-css import included`);
1166
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1078
1167
  }
1079
1168
  }
1080
1169
  }
1081
1170
  } else {
1082
1171
  // 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);
1172
+ const normalized = normalizeAndCleanCSS(processedCssContent, hasTwAnimate);
1086
1173
  fs.writeFileSync(fullCssPath, normalized);
1087
1174
  if (!silent) {
1088
1175
  success(`Created ${cssPath} with Weloop styles`);
1089
1176
  if (hasTwAnimate) {
1090
- info(` tw-animate-css import included`);
1177
+ info(` ${PACKAGE_NAMES.TW_ANIMATE_CSS} import included`);
1091
1178
  }
1092
1179
  }
1093
1180
  }
1094
1181
  } catch (err) {
1095
1182
  if (!silent) {
1096
1183
  warn(`Could not automatically install CSS styles: ${err.message}`);
1097
- info(`\n To add styles manually:`);
1098
- info(` 1. Download: ${registryUrl.replace('/registry', '/app/globals.css')}`);
1099
- info(` 2. Add the CSS variables to your ${cssPath} file`);
1100
- 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;
1101
1189
  }
1102
1190
  }
1103
1191
  }
@@ -1116,11 +1204,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
1116
1204
  */
1117
1205
  function createUtilsFile(utilsPath) {
1118
1206
  if (fs.existsSync(utilsPath)) return;
1119
-
1207
+
1120
1208
  // Silently create utils file (matching shadcn/ui behavior)
1121
1209
  // Don't overwrite if user has customized it
1122
1210
  ensureDirectoryExists(path.dirname(utilsPath));
1123
-
1211
+
1124
1212
  const utilsContent = `import { clsx, type ClassValue } from "clsx"
1125
1213
  import { twMerge } from "tailwind-merge"
1126
1214
 
@@ -1150,47 +1238,41 @@ export function cn(...inputs: ClassValue[]) {
1150
1238
  */
1151
1239
  async function installComponent(componentName, options = {}) {
1152
1240
  const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
1153
-
1241
+
1154
1242
  // Step 1: Install base dependencies (required for utils.ts to work)
1155
- const baseDeps = [];
1156
- if (!checkPackageInstalled('clsx')) {
1157
- baseDeps.push('clsx');
1158
- }
1159
- if (!checkPackageInstalled('tailwind-merge')) {
1160
- baseDeps.push('tailwind-merge');
1161
- }
1243
+ const baseDeps = BASE_DEPENDENCIES.filter(dep => !checkPackageInstalled(dep));
1162
1244
  if (baseDeps.length > 0) {
1163
1245
  info(`Installing base dependencies: ${baseDeps.join(', ')}...`);
1164
1246
  await installPackages(baseDeps);
1165
1247
  }
1166
-
1248
+
1167
1249
  // Step 2: Load or create components.json configuration
1168
1250
  const config = loadComponentsConfig();
1169
-
1251
+
1170
1252
  // Step 3: Install tw-animate-css automatically (required for component animations)
1171
- if (!checkPackageInstalled('tw-animate-css')) {
1172
- info('Installing tw-animate-css for animations...');
1173
- 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]);
1174
1256
  }
1175
-
1257
+
1176
1258
  // Step 4: Install CSS styles early (before component installation)
1177
1259
  // Silent mode prevents output when styles are already installed
1178
1260
  await installCSSStyles(config, registryUrl, false, true);
1179
-
1261
+
1180
1262
  // Step 5: Resolve paths from components.json configuration
1181
1263
  const uiAlias = config.aliases?.ui || '@/components/ui';
1182
1264
  const utilsAlias = config.aliases?.utils || '@/lib/utils';
1183
-
1265
+
1184
1266
  const componentsDir = path.join(process.cwd(), uiAlias.replace('@/', '').replace(/^\/+/, ''));
1185
1267
  let utilsPath = utilsAlias.replace('@/', '').replace(/^\/+/, '');
1186
1268
  if (!utilsPath.endsWith('.ts') && !utilsPath.endsWith('.tsx')) {
1187
1269
  utilsPath = utilsPath + '.ts';
1188
1270
  }
1189
1271
  utilsPath = path.join(process.cwd(), utilsPath);
1190
-
1272
+
1191
1273
  // Step 6: Load component registry from remote or local source
1192
1274
  const registry = await loadRegistry(componentName, registryUrl);
1193
-
1275
+
1194
1276
  if (!registry) {
1195
1277
  error(`Component "${componentName}" not found in registry.`);
1196
1278
  info('Available components:');
@@ -1249,6 +1331,244 @@ async function installComponent(componentName, options = {}) {
1249
1331
  success(`\nSuccessfully installed "${componentName}"`);
1250
1332
  }
1251
1333
 
1334
+ // ============================================================================
1335
+ // INIT COMMAND - Project Initialization
1336
+ // ============================================================================
1337
+
1338
+ /**
1339
+ * Detects the project type (Next.js or Vite)
1340
+ * @returns {string} 'nextjs' or 'vite' or null
1341
+ */
1342
+ function detectProjectType() {
1343
+ // Check for Next.js
1344
+ if (fs.existsSync(path.join(process.cwd(), 'next.config.js')) ||
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'))) {
1349
+ return 'nextjs';
1350
+ }
1351
+
1352
+ // Check for Vite
1353
+ if (fs.existsSync(path.join(process.cwd(), 'vite.config.js')) ||
1354
+ fs.existsSync(path.join(process.cwd(), 'vite.config.ts')) ||
1355
+ fs.existsSync(path.join(process.cwd(), 'vite.config.mjs'))) {
1356
+ return 'vite';
1357
+ }
1358
+
1359
+ return null;
1360
+ }
1361
+
1362
+ /**
1363
+ * Prompts user for configuration using readline
1364
+ * @param {string} question - Question to ask
1365
+ * @param {string} defaultValue - Default value
1366
+ * @returns {Promise<string>} User input
1367
+ */
1368
+ function prompt(question, defaultValue = '') {
1369
+ return new Promise((resolve) => {
1370
+ const readline = require('readline');
1371
+ const rl = readline.createInterface({
1372
+ input: process.stdin,
1373
+ output: process.stdout
1374
+ });
1375
+
1376
+ const promptText = defaultValue
1377
+ ? `${question} › ${defaultValue} `
1378
+ : `${question} › `;
1379
+
1380
+ rl.question(promptText, (answer) => {
1381
+ rl.close();
1382
+ resolve(answer.trim() || defaultValue);
1383
+ });
1384
+ });
1385
+ }
1386
+
1387
+ /**
1388
+ * Initializes the project with interactive prompts
1389
+ * Similar to shadcn/ui init command but with custom theme selection
1390
+ */
1391
+ async function initProject() {
1392
+ info('Initializing project...\n');
1393
+
1394
+ // Check if components.json already exists
1395
+ const configPath = path.join(process.cwd(), PATHS.COMPONENTS_JSON);
1396
+ if (fs.existsSync(configPath)) {
1397
+ warn(`${PATHS.COMPONENTS_JSON} already exists.`);
1398
+ const overwrite = await prompt('Do you want to overwrite it? (y/N)', 'N');
1399
+ if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
1400
+ info('Skipping initialization.');
1401
+ return;
1402
+ }
1403
+ }
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;
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
+ // Detect CSS path based on project type
1497
+ let cssPath = PATHS.APP_GLOBALS_CSS;
1498
+ if (isVite) {
1499
+ cssPath = PATHS.VITE_INDEX_CSS;
1500
+ } else if (fs.existsSync(path.join(process.cwd(), 'app', 'globals.css'))) {
1501
+ cssPath = PATHS.APP_GLOBALS_CSS;
1502
+ } else if (fs.existsSync(path.join(process.cwd(), 'styles', 'globals.css'))) {
1503
+ cssPath = PATHS.STYLES_GLOBALS_CSS;
1504
+ } else {
1505
+ cssPath = await prompt('Where is your global CSS file?', cssPath);
1506
+ }
1507
+
1508
+ // Create components.json with selected theme
1509
+ const config = {
1510
+ $schema: DEFAULTS.SCHEMA_URL,
1511
+ style: selectedTheme,
1512
+ rsc: isNextjs,
1513
+ tsx: true,
1514
+ tailwind: {
1515
+ config: 'tailwind.config.ts',
1516
+ css: cssPath,
1517
+ baseColor: 'neutral',
1518
+ cssVariables: true,
1519
+ prefix: ''
1520
+ },
1521
+ iconLibrary: DEFAULTS.ICON_LIBRARY,
1522
+ aliases: {
1523
+ components: '@/components',
1524
+ utils: '@/lib/utils',
1525
+ ui: '@/components/ui',
1526
+ lib: '@/lib',
1527
+ hooks: '@/hooks'
1528
+ },
1529
+ registry: REGISTRY_URLS.INDEX,
1530
+ projectName: projectName,
1531
+ radius: selectedRadius
1532
+ };
1533
+
1534
+ // Show setup progress
1535
+ info('\nšŸš€ Setting up your project...');
1536
+
1537
+ // Step 1: Create components.json
1538
+ info(' → [:root]/components.json');
1539
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1540
+
1541
+ // Step 2: Update CSS variables
1542
+ info(' → [:root]/app/globals.css (Updating css variables)');
1543
+ await installCSSStyles(config, DEFAULT_REGISTRY_URL, false, false);
1544
+
1545
+ // Step 3: Install dependencies
1546
+ info(' → Installing dependencies');
1547
+ const missingBaseDeps = BASE_DEPENDENCIES.filter(dep => !checkPackageInstalled(dep));
1548
+
1549
+ if (missingBaseDeps.length > 0) {
1550
+ await installPackages(missingBaseDeps);
1551
+ }
1552
+
1553
+ // Install tw-animate-css for animations
1554
+ if (!checkPackageInstalled(PACKAGE_NAMES.TW_ANIMATE_CSS)) {
1555
+ await installPackages([PACKAGE_NAMES.TW_ANIMATE_CSS]);
1556
+ }
1557
+
1558
+ // Step 4: Create utils.ts file
1559
+ info(' → Create 1 files:');
1560
+ const utilsPath = path.join(process.cwd(), 'lib', 'utils.ts');
1561
+ createUtilsFile(utilsPath);
1562
+ info(' → libs/utils.ts');
1563
+
1564
+ success('\nāœ… Success! Successfully initialization.');
1565
+ info('\nYou could add the components now.');
1566
+ info('\nAvailable commands:');
1567
+ info(' npx weloop-kosign@latest add <component-name>');
1568
+ info(' pnpm dlx weloop-kosign@latest add <component-name>');
1569
+ info(' yarn weloop-kosign@latest add <component-name>\n');
1570
+ }
1571
+
1252
1572
  // ============================================================================
1253
1573
  // COMMANDS - CLI Command Handlers
1254
1574
  // ============================================================================
@@ -1262,7 +1582,7 @@ async function installComponent(componentName, options = {}) {
1262
1582
  async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
1263
1583
  try {
1264
1584
  const index = await loadIndex(registryUrl);
1265
-
1585
+
1266
1586
  console.log('\nAvailable components:\n');
1267
1587
  index.registry.forEach(comp => {
1268
1588
  // Show dependencies if component has any
@@ -1298,13 +1618,13 @@ async function main() {
1298
1618
  const args = process.argv.slice(2);
1299
1619
  const command = args[0];
1300
1620
  const componentName = args[1];
1301
-
1621
+
1302
1622
  // Parse --registry option if provided
1303
1623
  const registryIndex = args.indexOf('--registry');
1304
1624
  const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
1305
1625
  ? args[registryIndex + 1]
1306
1626
  : DEFAULT_REGISTRY_URL;
1307
-
1627
+
1308
1628
  // Build options object
1309
1629
  const options = {
1310
1630
  overwrite: args.includes('--overwrite') || args.includes('-f'),
@@ -1315,6 +1635,7 @@ async function main() {
1315
1635
  if (!command) {
1316
1636
  console.log(`
1317
1637
  Usage:
1638
+ node scripts/cli-remote.js init [--registry <url>] Initialize project
1318
1639
  node scripts/cli-remote.js add <component-name> [--registry <url>] Install a component
1319
1640
  node scripts/cli-remote.js list [--registry <url>] List all available components
1320
1641
  node scripts/cli-remote.js css [--registry <url>] [--overwrite] Install/update CSS styles
@@ -1324,6 +1645,7 @@ Options:
1324
1645
  --overwrite, -f Overwrite existing files
1325
1646
 
1326
1647
  Examples:
1648
+ node scripts/cli-remote.js init
1327
1649
  node scripts/cli-remote.js add button
1328
1650
  node scripts/cli-remote.js add button --registry https://raw.githubusercontent.com/user/repo/main/registry
1329
1651
  node scripts/cli-remote.js list
@@ -1335,6 +1657,10 @@ Examples:
1335
1657
 
1336
1658
  // Route to appropriate command handler
1337
1659
  switch (command) {
1660
+ case 'init':
1661
+ await initProject();
1662
+ break;
1663
+
1338
1664
  case 'add':
1339
1665
  if (!componentName) {
1340
1666
  error('Please provide a component name');
@@ -1343,21 +1669,21 @@ Examples:
1343
1669
  }
1344
1670
  await installComponent(componentName, options);
1345
1671
  break;
1346
-
1672
+
1347
1673
  case 'list':
1348
1674
  await listComponents(registryUrl);
1349
1675
  break;
1350
-
1676
+
1351
1677
  case 'css':
1352
1678
  case 'styles':
1353
1679
  // Both 'css' and 'styles' commands do the same thing
1354
1680
  const config = loadComponentsConfig();
1355
1681
  await installCSSStyles(config, registryUrl, options.overwrite, false);
1356
1682
  break;
1357
-
1683
+
1358
1684
  default:
1359
1685
  error(`Unknown command: ${command}`);
1360
- console.log('Available commands: add, list, css');
1686
+ console.log('Available commands: init, add, list, css');
1361
1687
  process.exit(1);
1362
1688
  }
1363
1689
  }