weloop-kosign 1.0.2

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.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Weloop Component Library
2
+
3
+ A modular component library built with Next.js, similar to shadcn/ui. Install only the components you need directly into your project.
4
+
5
+ ## Installation
6
+
7
+ First, make sure you have the base dependencies installed:
8
+
9
+ ```bash
10
+ npm install clsx tailwind-merge
11
+ ```
12
+
13
+ Then install components using the CLI:
14
+
15
+ ```bash
16
+ npx weloop-kosign@latest add button
17
+ ```
18
+
19
+ To install the base CSS styles:
20
+
21
+ ```bash
22
+ npx weloop-kosign@latest css
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Once you've installed a component, you can import and use it in your code:
28
+
29
+ ```tsx
30
+ import { Button } from "@/components/ui/button";
31
+
32
+ export function MyComponent() {
33
+ return <Button>Click me</Button>;
34
+ }
35
+ ```
36
+
37
+ The CLI will automatically install any required dependencies for each component. If something's missing, it'll let you know.
38
+
39
+ ## Available Commands
40
+
41
+ Install a component:
42
+
43
+ ```bash
44
+ npx weloop-kosign@latest add <component-name>
45
+ ```
46
+
47
+ See all available components:
48
+
49
+ ```bash
50
+ npx weloop-kosign@latest list
51
+ ```
52
+
53
+ Install or update CSS styles:
54
+
55
+ ```bash
56
+ npx weloop-kosign@latest css
57
+ ```
58
+
59
+ Overwrite an existing component:
60
+
61
+ ```bash
62
+ npx weloop-kosign@latest add button --overwrite
63
+ ```
64
+
65
+ ## Component Dependencies
66
+
67
+ Some components depend on others. For example, the calendar component needs the button component. The CLI handles this automatically, so you don't need to worry about installing dependencies manually.
68
+
69
+ ## Local Development
70
+
71
+ If you're working on this project itself, you can use the npm scripts:
72
+
73
+ ```bash
74
+ npm run add button
75
+ npm run components:list
76
+ npm run generate-registry
77
+ ```
78
+
79
+ ## Project Structure
80
+
81
+ - Components live in `components/ui/`
82
+ - Registry files are in `registry/` - these contain the component metadata
83
+ - CLI scripts are in `scripts/`
84
+
85
+ ## Development
86
+
87
+ Run the development server:
88
+
89
+ ```bash
90
+ npm run dev
91
+ ```
92
+
93
+ Then open [http://localhost:3000](http://localhost:3000) in your browser.
94
+
95
+ ## Building
96
+
97
+ Build the project:
98
+
99
+ ```bash
100
+ npm run build
101
+ ```
102
+
103
+ Start the production server:
104
+
105
+ ```bash
106
+ npm start
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "weloop-kosign",
3
+ "version": "1.0.2",
4
+ "description": "CLI tool for installing Weloop UI components",
5
+ "keywords": [
6
+ "weloop",
7
+ "ui",
8
+ "components",
9
+ "cli",
10
+ "shadcn"
11
+ ],
12
+ "author": "KOSIGN",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app.git"
17
+ },
18
+ "homepage": "https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app",
19
+ "private": false,
20
+ "bin": {
21
+ "weloop-kosign": "./scripts/cli-remote.js"
22
+ },
23
+ "files": [
24
+ "scripts/cli-remote.js",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "dev": "next dev",
29
+ "build": "next build",
30
+ "start": "next start",
31
+ "lint": "eslint",
32
+ "generate-registry": "node scripts/generate-all-registry.js",
33
+ "add": "node scripts/cli-remote.js add",
34
+ "components:list": "node scripts/cli-remote.js list"
35
+ },
36
+ "dependencies": {
37
+ "@dnd-kit/core": "^6.3.1",
38
+ "@dnd-kit/modifiers": "^9.0.0",
39
+ "@dnd-kit/sortable": "^10.0.0",
40
+ "@dnd-kit/utilities": "^3.2.2",
41
+ "@hookform/resolvers": "^5.2.2",
42
+ "@lobehub/icons": "^2.43.1",
43
+ "@radix-ui/react-accordion": "^1.2.12",
44
+ "@radix-ui/react-alert-dialog": "^1.1.15",
45
+ "@radix-ui/react-aspect-ratio": "^1.1.8",
46
+ "@radix-ui/react-avatar": "^1.1.11",
47
+ "@radix-ui/react-checkbox": "^1.3.3",
48
+ "@radix-ui/react-collapsible": "^1.1.12",
49
+ "@radix-ui/react-context-menu": "^2.2.16",
50
+ "@radix-ui/react-dialog": "^1.1.15",
51
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
52
+ "@radix-ui/react-hover-card": "^1.1.15",
53
+ "@radix-ui/react-label": "^2.1.8",
54
+ "@radix-ui/react-menubar": "^1.1.16",
55
+ "@radix-ui/react-navigation-menu": "^1.2.14",
56
+ "@radix-ui/react-popover": "^1.1.15",
57
+ "@radix-ui/react-radio-group": "^1.3.8",
58
+ "@radix-ui/react-scroll-area": "^1.2.10",
59
+ "@radix-ui/react-select": "^2.2.6",
60
+ "@radix-ui/react-separator": "^1.1.8",
61
+ "@radix-ui/react-slider": "^1.3.6",
62
+ "@radix-ui/react-slot": "^1.2.4",
63
+ "@radix-ui/react-switch": "^1.2.6",
64
+ "@radix-ui/react-tabs": "^1.1.13",
65
+ "@radix-ui/react-toggle": "^1.1.10",
66
+ "@radix-ui/react-toggle-group": "^1.1.11",
67
+ "@radix-ui/react-tooltip": "^1.2.8",
68
+ "@tabler/icons-react": "^3.35.0",
69
+ "@tanstack/react-table": "^8.21.3",
70
+ "class-variance-authority": "^0.7.1",
71
+ "clsx": "^2.1.1",
72
+ "cmdk": "^1.1.1",
73
+ "date-fns": "^4.1.0",
74
+ "embla-carousel-react": "^8.6.0",
75
+ "input-otp": "^1.4.2",
76
+ "lucide-react": "^0.554.0",
77
+ "next": "^16.0.7",
78
+ "next-themes": "^0.4.6",
79
+ "ogl": "^1.0.11",
80
+ "react": "19.2.0",
81
+ "react-day-picker": "^9.11.3",
82
+ "react-dom": "19.2.0",
83
+ "react-hook-form": "^7.68.0",
84
+ "react-resizable-panels": "^4.0.1",
85
+ "recharts": "^2.15.4",
86
+ "shiki": "^3.20.0",
87
+ "sonner": "^2.0.7",
88
+ "tailwind-merge": "^3.4.0",
89
+ "three": "^0.167.1",
90
+ "vaul": "^1.1.2",
91
+ "zod": "^4.2.1"
92
+ },
93
+ "devDependencies": {
94
+ "@netlify/plugin-nextjs": "^4.39.0",
95
+ "@tailwindcss/postcss": "^4",
96
+ "@types/node": "^20",
97
+ "@types/react": "^19",
98
+ "@types/react-dom": "^19",
99
+ "@types/three": "^0.182.0",
100
+ "eslint": "^9",
101
+ "eslint-config-next": "16.0.3",
102
+ "tailwindcss": "^4",
103
+ "tw-animate-css": "^1.4.0",
104
+ "typescript": "^5"
105
+ }
106
+ }
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI tool for installing components from a registry (local or remote)
5
+ *
6
+ * Auto-detects local vs remote:
7
+ * - If running in the component library project: uses local registry/ directory
8
+ * - If running in external projects: uses remote GitLab/GitHub URL
9
+ *
10
+ * Usage (via npx - recommended):
11
+ * npx weloop-kosign@latest add <component-name>
12
+ * npx weloop-kosign@latest list
13
+ * npx weloop-kosign@latest css [--overwrite]
14
+ *
15
+ * Usage (local development):
16
+ * node scripts/cli-remote.js add <component-name> [--registry <url|path>]
17
+ * node scripts/cli-remote.js list [--registry <url|path>]
18
+ * node scripts/cli-remote.js css [--registry <url|path>] [--overwrite]
19
+ *
20
+ * Examples:
21
+ * npx weloop-kosign@latest add button
22
+ * npx weloop-kosign@latest add button --registry ./registry
23
+ * npx weloop-kosign@latest css
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const https = require('https');
29
+ const http = require('http');
30
+ const { execSync } = require('child_process');
31
+
32
+ // ============================================================================
33
+ // CONFIGURATION
34
+ // ============================================================================
35
+
36
+ function getDefaultRegistryUrl() {
37
+ const localRegistryPath = path.join(__dirname, '../registry');
38
+ if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
39
+ return localRegistryPath;
40
+ }
41
+ return process.env.WELOOP_REGISTRY_URL ||
42
+ 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
43
+ }
44
+
45
+ const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
46
+
47
+ // ============================================================================
48
+ // UTILITIES
49
+ // ============================================================================
50
+
51
+ const colors = {
52
+ reset: '\x1b[0m',
53
+ green: '\x1b[32m',
54
+ red: '\x1b[31m',
55
+ yellow: '\x1b[33m',
56
+ blue: '\x1b[34m',
57
+ cyan: '\x1b[36m',
58
+ };
59
+
60
+ function log(message, color = 'reset') {
61
+ console.log(`${colors[color]}${message}${colors.reset}`);
62
+ }
63
+
64
+ function error(message) {
65
+ log(`Error: ${message}`, 'red');
66
+ }
67
+
68
+ function success(message) {
69
+ log(message, 'green');
70
+ }
71
+
72
+ function info(message) {
73
+ log(message, 'blue');
74
+ }
75
+
76
+ function warn(message) {
77
+ log(`Warning: ${message}`, 'yellow');
78
+ }
79
+
80
+ function ensureDirectoryExists(dirPath) {
81
+ if (!fs.existsSync(dirPath)) {
82
+ fs.mkdirSync(dirPath, { recursive: true });
83
+ }
84
+ }
85
+
86
+ function isLocalPath(pathOrUrl) {
87
+ return !pathOrUrl.startsWith('http://') && !pathOrUrl.startsWith('https://');
88
+ }
89
+
90
+ // ============================================================================
91
+ // PACKAGE MANAGEMENT
92
+ // ============================================================================
93
+
94
+ function detectPackageManager() {
95
+ if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
96
+ if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
97
+ if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
98
+
99
+ const userAgent = process.env.npm_config_user_agent || '';
100
+ if (userAgent.includes('yarn')) return 'yarn';
101
+ if (userAgent.includes('pnpm')) return 'pnpm';
102
+
103
+ return 'npm';
104
+ }
105
+
106
+ function getInstallCommand(packageManager, packages) {
107
+ const packagesStr = packages.join(' ');
108
+ switch (packageManager) {
109
+ case 'yarn': return `yarn add ${packagesStr}`;
110
+ case 'pnpm': return `pnpm add ${packagesStr}`;
111
+ case 'npm':
112
+ default: return `npm install ${packagesStr}`;
113
+ }
114
+ }
115
+
116
+ function checkPackageInstalled(packageName) {
117
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
118
+ if (!fs.existsSync(packageJsonPath)) return false;
119
+
120
+ try {
121
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
122
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
123
+ return !!allDeps[packageName];
124
+ } catch (e) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function getMissingDependencies(requiredDeps) {
130
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
131
+ if (!fs.existsSync(packageJsonPath)) return requiredDeps;
132
+
133
+ try {
134
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
135
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
136
+
137
+ return requiredDeps.filter(dep => {
138
+ const depName = dep.split('/').slice(0, 2).join('/');
139
+ return !allDeps[dep] && !allDeps[depName];
140
+ });
141
+ } catch (e) {
142
+ return requiredDeps;
143
+ }
144
+ }
145
+
146
+ async function installPackages(packages) {
147
+ if (packages.length === 0) return;
148
+
149
+ const packageManager = detectPackageManager();
150
+ info(`\nInstalling dependencies: ${packages.join(', ')}`);
151
+
152
+ try {
153
+ const installCmd = getInstallCommand(packageManager, packages);
154
+ execSync(installCmd, { stdio: 'inherit', cwd: process.cwd() });
155
+ success(`Dependencies installed successfully`);
156
+ } catch (error) {
157
+ warn(`Failed to install dependencies automatically`);
158
+ console.log(`\n Please install them manually:`);
159
+ console.log(` ${getInstallCommand(packageManager, packages)}\n`);
160
+ }
161
+ }
162
+
163
+ // ============================================================================
164
+ // FILE OPERATIONS
165
+ // ============================================================================
166
+
167
+ function loadComponentsConfig() {
168
+ const configPath = path.join(process.cwd(), 'components.json');
169
+
170
+ if (!fs.existsSync(configPath)) {
171
+ error('components.json not found. Please initialize your project first.');
172
+ info('Create a components.json file with your project configuration.');
173
+ process.exit(1);
174
+ }
175
+
176
+ const content = fs.readFileSync(configPath, 'utf-8');
177
+ return JSON.parse(content);
178
+ }
179
+
180
+ async function fetchJSON(urlOrPath, retries = 3) {
181
+ if (isLocalPath(urlOrPath)) {
182
+ return new Promise((resolve, reject) => {
183
+ try {
184
+ const fullPath = path.isAbsolute(urlOrPath)
185
+ ? urlOrPath
186
+ : path.join(process.cwd(), urlOrPath);
187
+
188
+ if (!fs.existsSync(fullPath)) {
189
+ reject(new Error(`File not found: ${fullPath}`));
190
+ return;
191
+ }
192
+
193
+ const content = fs.readFileSync(fullPath, 'utf-8');
194
+ resolve(JSON.parse(content));
195
+ } catch (e) {
196
+ reject(new Error(`Failed to read local file: ${e.message}`));
197
+ }
198
+ });
199
+ }
200
+
201
+ return new Promise((resolve, reject) => {
202
+ const client = urlOrPath.startsWith('https') ? https : http;
203
+ let timeout;
204
+ let request;
205
+
206
+ const makeRequest = () => {
207
+ request = client.get(urlOrPath, (res) => {
208
+ clearTimeout(timeout);
209
+
210
+ if (res.statusCode === 302 || res.statusCode === 301) {
211
+ return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
212
+ }
213
+
214
+ if (res.statusCode === 403) {
215
+ reject(new Error(`Access forbidden (403). Repository may be private. Make it public in GitLab/GitHub settings, or use --registry with a public URL.`));
216
+ return;
217
+ }
218
+
219
+ if (res.statusCode === 404) {
220
+ reject(new Error(`Not found (404). Check that the registry files are pushed to the repository and the URL is correct.`));
221
+ return;
222
+ }
223
+
224
+ if (res.statusCode !== 200) {
225
+ reject(new Error(`Failed to fetch: HTTP ${res.statusCode} - ${res.statusMessage || 'Unknown error'}`));
226
+ return;
227
+ }
228
+
229
+ let data = '';
230
+ res.on('data', (chunk) => { data += chunk; });
231
+ res.on('end', () => {
232
+ try {
233
+ if (data.trim().startsWith('<')) {
234
+ reject(new Error('Received HTML instead of JSON. Repository may be private or URL incorrect.'));
235
+ return;
236
+ }
237
+ resolve(JSON.parse(data));
238
+ } catch (e) {
239
+ reject(new Error(`Invalid JSON response: ${e.message}`));
240
+ }
241
+ });
242
+ });
243
+
244
+ request.on('error', (err) => {
245
+ clearTimeout(timeout);
246
+ if (retries > 0 && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND')) {
247
+ info(`Connection error, retrying... (${retries} attempts left)`);
248
+ setTimeout(() => {
249
+ fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
250
+ }, 1000);
251
+ } else {
252
+ reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
253
+ }
254
+ });
255
+
256
+ timeout = setTimeout(() => {
257
+ request.destroy();
258
+ if (retries > 0) {
259
+ info(`Request timeout, retrying... (${retries} attempts left)`);
260
+ setTimeout(() => {
261
+ fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
262
+ }, 1000);
263
+ } else {
264
+ reject(new Error('Request timeout after multiple attempts'));
265
+ }
266
+ }, 15000);
267
+ };
268
+
269
+ makeRequest();
270
+ });
271
+ }
272
+
273
+ async function fetchText(urlOrPath) {
274
+ if (isLocalPath(urlOrPath)) {
275
+ if (!fs.existsSync(urlOrPath)) {
276
+ throw new Error(`File not found: ${urlOrPath}`);
277
+ }
278
+ return fs.readFileSync(urlOrPath, 'utf-8');
279
+ }
280
+
281
+ return new Promise((resolve, reject) => {
282
+ const client = urlOrPath.startsWith('https') ? https : http;
283
+ let data = '';
284
+
285
+ const req = client.get(urlOrPath, (res) => {
286
+ if (res.statusCode === 302 || res.statusCode === 301) {
287
+ return fetchText(res.headers.location).then(resolve).catch(reject);
288
+ }
289
+
290
+ if (res.statusCode === 403) {
291
+ reject(new Error('Access forbidden - repository may be private'));
292
+ return;
293
+ }
294
+
295
+ if (res.statusCode === 404) {
296
+ reject(new Error('CSS file not found in repository'));
297
+ return;
298
+ }
299
+
300
+ if (res.statusCode !== 200) {
301
+ reject(new Error(`HTTP ${res.statusCode}`));
302
+ return;
303
+ }
304
+
305
+ res.on('data', (chunk) => { data += chunk; });
306
+ res.on('end', () => {
307
+ if (data.trim().startsWith('<')) {
308
+ reject(new Error('Received HTML instead of CSS'));
309
+ return;
310
+ }
311
+ resolve(data);
312
+ });
313
+ });
314
+
315
+ req.on('error', (err) => {
316
+ if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
317
+ setTimeout(() => {
318
+ fetchText(urlOrPath).then(resolve).catch(reject);
319
+ }, 1000);
320
+ } else {
321
+ reject(err);
322
+ }
323
+ });
324
+
325
+ req.setTimeout(15000, () => {
326
+ req.destroy();
327
+ reject(new Error('Request timeout'));
328
+ });
329
+ });
330
+ }
331
+
332
+ function resolveRegistryPath(baseUrl, fileName) {
333
+ if (isLocalPath(baseUrl)) {
334
+ const basePath = path.isAbsolute(baseUrl)
335
+ ? baseUrl
336
+ : path.join(process.cwd(), baseUrl);
337
+ return path.join(basePath, fileName);
338
+ }
339
+ return `${baseUrl}/${fileName}`;
340
+ }
341
+
342
+ async function loadRegistry(componentName, registryBaseUrl) {
343
+ try {
344
+ const registryPath = resolveRegistryPath(registryBaseUrl, `${componentName}.json`);
345
+ return await fetchJSON(registryPath);
346
+ } catch (err) {
347
+ return null;
348
+ }
349
+ }
350
+
351
+ async function loadIndex(registryBaseUrl) {
352
+ const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
353
+ try {
354
+ return await fetchJSON(indexPath);
355
+ } catch (err) {
356
+ error(`Failed to load registry index from ${indexPath}`);
357
+ throw err;
358
+ }
359
+ }
360
+
361
+ // ============================================================================
362
+ // CSS PROCESSING
363
+ // ============================================================================
364
+
365
+ function hasWeloopStyles(content) {
366
+ return content.includes('--WLDS-PRM-') ||
367
+ content.includes('--WLDS-RED-') ||
368
+ content.includes('--WLDS-NTL-') ||
369
+ content.includes('--system-100') ||
370
+ content.includes('--system-200');
371
+ }
372
+
373
+ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
374
+ const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
375
+ let processed = cssContent;
376
+
377
+ if (!hasTwAnimate) {
378
+ processed = cssContent.replace(twAnimatePattern, '');
379
+ if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
380
+ if (forceUpdate) {
381
+ warn('tw-animate-css package not found - removed from CSS to prevent build errors');
382
+ info(' Install with: npm install tw-animate-css');
383
+ info(' Then run: node scripts/weloop-cli.js css --overwrite to include it');
384
+ } else {
385
+ warn('tw-animate-css package not found - removed from CSS');
386
+ info(' Install with: npm install tw-animate-css');
387
+ info(' Or add the import manually after installing the package');
388
+ }
389
+ }
390
+ } else {
391
+ // Ensure import exists if package is installed
392
+ if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
393
+ if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
394
+ processed = processed.replace(
395
+ /(@import\s+["']tailwindcss["'];?\s*\n?)/,
396
+ '$1@import "tw-animate-css";\n'
397
+ );
398
+ } else {
399
+ processed = '@import "tw-animate-css";\n' + processed;
400
+ }
401
+ }
402
+ }
403
+
404
+ return processed;
405
+ }
406
+
407
+ function removeTailwindImport(cssContent) {
408
+ return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
409
+ }
410
+
411
+ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
412
+ if (!hasTwAnimate || cssContent.includes('@import "tw-animate-css"')) {
413
+ return cssContent;
414
+ }
415
+ return '@import "tw-animate-css";\n' + cssContent;
416
+ }
417
+
418
+ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
419
+ const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
420
+ if (!tailwindMatch) {
421
+ return weloopStyles + '\n\n' + existing;
422
+ }
423
+
424
+ const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
425
+ const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
426
+
427
+ let importsToAdd = '';
428
+ if (hasTwAnimate && !existing.includes('@import "tw-animate-css"')) {
429
+ importsToAdd = '@import "tw-animate-css";\n';
430
+ }
431
+
432
+ return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStyles + '\n' + afterTailwind;
433
+ }
434
+
435
+ function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
436
+ const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
437
+ const imports = existing.match(importPattern) || [];
438
+ const importsText = imports.join('');
439
+
440
+ const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
441
+ const weloopStartMatch = existing.search(weloopStartPattern);
442
+
443
+ if (weloopStartMatch === -1) {
444
+ return existing + '\n\n' + newStyles;
445
+ }
446
+
447
+ let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
448
+ const nonWeloopImports = importsText.split('\n').filter(imp =>
449
+ !imp.includes('tw-animate-css') || hasTwAnimate
450
+ ).join('\n');
451
+ contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
452
+
453
+ return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
454
+ }
455
+
456
+ async function fetchCSSFromRegistry(registryUrl) {
457
+ let sourceCssPath;
458
+
459
+ if (isLocalPath(registryUrl)) {
460
+ const basePath = path.isAbsolute(registryUrl)
461
+ ? path.dirname(registryUrl)
462
+ : path.join(process.cwd(), path.dirname(registryUrl));
463
+ sourceCssPath = path.join(basePath, 'app', 'globals.css');
464
+ } else {
465
+ const baseUrl = registryUrl.replace('/registry', '');
466
+ sourceCssPath = `${baseUrl}/app/globals.css`;
467
+ }
468
+
469
+ return await fetchText(sourceCssPath);
470
+ }
471
+
472
+ async function installCSSStyles(config, registryUrl, forceUpdate = false) {
473
+ const cssPath = config.tailwind?.css || 'app/globals.css';
474
+ const fullCssPath = path.join(process.cwd(), cssPath);
475
+
476
+ // Check if Weloop styles already exist
477
+ let hasWeloopStylesInFile = false;
478
+ if (fs.existsSync(fullCssPath)) {
479
+ const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
480
+ hasWeloopStylesInFile = hasWeloopStyles(existingContent);
481
+
482
+ if (forceUpdate && hasWeloopStylesInFile) {
483
+ info('Updating existing Weloop styles...');
484
+ }
485
+ }
486
+
487
+ try {
488
+ info('Installing CSS styles...');
489
+ const cssContent = await fetchCSSFromRegistry(registryUrl);
490
+ ensureDirectoryExists(path.dirname(fullCssPath));
491
+
492
+ const hasTwAnimate = checkPackageInstalled('tw-animate-css');
493
+ let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
494
+
495
+ // Handle file installation
496
+ if (forceUpdate && fs.existsSync(fullCssPath)) {
497
+ // --overwrite: Replace entire file
498
+ fs.writeFileSync(fullCssPath, processedCssContent);
499
+ success(`Overwritten ${cssPath} with Weloop styles`);
500
+ if (hasTwAnimate) {
501
+ info(` tw-animate-css import included`);
502
+ }
503
+ } else if (fs.existsSync(fullCssPath)) {
504
+ // Normal mode: Merge intelligently
505
+ const existing = fs.readFileSync(fullCssPath, 'utf-8');
506
+ const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
507
+ existing.includes('@tailwind base');
508
+
509
+ if (hasWeloopStylesInFile) {
510
+ // Replace existing Weloop styles
511
+ let weloopStyles = removeTailwindImport(processedCssContent);
512
+ weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
513
+ const finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
514
+ fs.writeFileSync(fullCssPath, finalContent);
515
+ success(`Updated ${cssPath} with Weloop styles`);
516
+ info(` Existing Weloop styles were replaced with latest version`);
517
+ } else if (hasTailwindImport) {
518
+ // Merge after Tailwind imports
519
+ let weloopStyles = removeTailwindImport(processedCssContent);
520
+ weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
521
+ const merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
522
+ fs.writeFileSync(fullCssPath, merged);
523
+ success(`Updated ${cssPath} with Weloop styles`);
524
+ if (hasTwAnimate) {
525
+ info(` tw-animate-css import included`);
526
+ }
527
+ info(` Your existing styles are preserved`);
528
+ } else {
529
+ // No Tailwind imports, prepend everything
530
+ let finalCssContent = processedCssContent;
531
+ if (hasTwAnimate && !finalCssContent.includes('@import "tw-animate-css"')) {
532
+ if (finalCssContent.includes('@import "tailwindcss"')) {
533
+ finalCssContent = finalCssContent.replace(
534
+ /(@import\s+["']tailwindcss["'];?\s*\n?)/,
535
+ '$1@import "tw-animate-css";\n'
536
+ );
537
+ } else {
538
+ finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
539
+ }
540
+ }
541
+ fs.writeFileSync(fullCssPath, finalCssContent + '\n\n' + existing);
542
+ success(`Updated ${cssPath} with Weloop styles`);
543
+ if (hasTwAnimate) {
544
+ info(` tw-animate-css import included`);
545
+ }
546
+ }
547
+ } else {
548
+ // Create new file
549
+ fs.writeFileSync(fullCssPath, processedCssContent);
550
+ success(`Created ${cssPath} with Weloop styles`);
551
+ if (hasTwAnimate) {
552
+ info(` tw-animate-css import included`);
553
+ }
554
+ }
555
+ } catch (err) {
556
+ warn(`Could not automatically install CSS styles: ${err.message}`);
557
+ info(`\n To add styles manually:`);
558
+ info(` 1. Download: ${registryUrl.replace('/registry', '/app/globals.css')}`);
559
+ info(` 2. Add the CSS variables to your ${cssPath} file`);
560
+ info(` 3. Or copy from: https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/app/globals.css\n`);
561
+ }
562
+ }
563
+
564
+ // ============================================================================
565
+ // COMPONENT INSTALLATION
566
+ // ============================================================================
567
+
568
+ function createUtilsFile(utilsPath) {
569
+ if (fs.existsSync(utilsPath)) return;
570
+
571
+ warn(`utils file not found at ${utilsPath}. Creating it...`);
572
+ ensureDirectoryExists(path.dirname(utilsPath));
573
+
574
+ const utilsContent = `import { clsx, type ClassValue } from "clsx"
575
+ import { twMerge } from "tailwind-merge"
576
+
577
+ export function cn(...inputs: ClassValue[]) {
578
+ return twMerge(clsx(inputs))
579
+ }
580
+ `;
581
+ fs.writeFileSync(utilsPath, utilsContent);
582
+ success(`Created ${path.relative(process.cwd(), utilsPath)}`);
583
+
584
+ // Check if required packages are installed
585
+ if (!checkPackageInstalled('clsx') || !checkPackageInstalled('tailwind-merge')) {
586
+ warn('utils.ts requires: clsx and tailwind-merge');
587
+ info(' Install with: npm install clsx tailwind-merge');
588
+ }
589
+ }
590
+
591
+ async function installComponent(componentName, options = {}) {
592
+ const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
593
+ const config = loadComponentsConfig();
594
+
595
+ // Install CSS styles early (before component installation)
596
+ await installCSSStyles(config, registryUrl);
597
+
598
+ // Get paths from components.json
599
+ const uiAlias = config.aliases?.ui || '@/components/ui';
600
+ const utilsAlias = config.aliases?.utils || '@/lib/utils';
601
+
602
+ const componentsDir = path.join(process.cwd(), uiAlias.replace('@/', '').replace(/^\/+/, ''));
603
+ let utilsPath = utilsAlias.replace('@/', '').replace(/^\/+/, '');
604
+ if (!utilsPath.endsWith('.ts') && !utilsPath.endsWith('.tsx')) {
605
+ utilsPath = utilsPath + '.ts';
606
+ }
607
+ utilsPath = path.join(process.cwd(), utilsPath);
608
+
609
+ // Load component registry
610
+ const registry = await loadRegistry(componentName, registryUrl);
611
+
612
+ if (!registry) {
613
+ error(`Component "${componentName}" not found in registry.`);
614
+ info('Available components:');
615
+ try {
616
+ const index = await loadIndex(registryUrl);
617
+ index.registry.forEach(comp => {
618
+ console.log(` - ${comp.name}`);
619
+ });
620
+ } catch (e) {
621
+ // Ignore if index fails
622
+ }
623
+ process.exit(1);
624
+ }
625
+
626
+ // Check if component already exists
627
+ const componentPath = path.join(componentsDir, `${componentName}.tsx`);
628
+ if (fs.existsSync(componentPath) && !overwrite) {
629
+ warn(`Component "${componentName}" already exists. Use --overwrite to replace it.`);
630
+ return;
631
+ }
632
+
633
+ // Install registry dependencies first
634
+ if (registry.registryDependencies && registry.registryDependencies.length > 0) {
635
+ info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
636
+ for (const dep of registry.registryDependencies) {
637
+ await installComponent(dep, { overwrite: false, registryUrl });
638
+ }
639
+ }
640
+
641
+ // Install component files
642
+ for (const file of registry.files) {
643
+ const filePath = path.join(process.cwd(), file.path);
644
+ ensureDirectoryExists(path.dirname(filePath));
645
+ fs.writeFileSync(filePath, file.content);
646
+ success(`Installed: ${file.path}`);
647
+ }
648
+
649
+ // Create utils.ts if needed
650
+ createUtilsFile(utilsPath);
651
+
652
+ // Install npm dependencies
653
+ if (registry.dependencies && registry.dependencies.length > 0) {
654
+ const missingDeps = getMissingDependencies(registry.dependencies);
655
+ if (missingDeps.length > 0) {
656
+ await installPackages(missingDeps);
657
+ } else {
658
+ info('\nAll dependencies are already installed');
659
+ }
660
+ }
661
+
662
+ success(`\nSuccessfully installed "${componentName}"`);
663
+ }
664
+
665
+ // ============================================================================
666
+ // COMMANDS
667
+ // ============================================================================
668
+
669
+ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
670
+ try {
671
+ const index = await loadIndex(registryUrl);
672
+
673
+ console.log('\nAvailable components:\n');
674
+ index.registry.forEach(comp => {
675
+ const deps = comp.registryDependencies && comp.registryDependencies.length > 0
676
+ ? ` (depends on: ${comp.registryDependencies.join(', ')})`
677
+ : '';
678
+ console.log(` ${comp.name}${deps}`);
679
+ });
680
+ console.log('');
681
+ } catch (err) {
682
+ error(`Failed to load components: ${err.message}`);
683
+ process.exit(1);
684
+ }
685
+ }
686
+
687
+ // ============================================================================
688
+ // MAIN
689
+ // ============================================================================
690
+
691
+ async function main() {
692
+ const args = process.argv.slice(2);
693
+ const command = args[0];
694
+ const componentName = args[1];
695
+
696
+ const registryIndex = args.indexOf('--registry');
697
+ const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
698
+ ? args[registryIndex + 1]
699
+ : DEFAULT_REGISTRY_URL;
700
+
701
+ const options = {
702
+ overwrite: args.includes('--overwrite') || args.includes('-f'),
703
+ registryUrl: registryUrl,
704
+ };
705
+
706
+ if (!command) {
707
+ console.log(`
708
+ Usage:
709
+ node scripts/cli-remote.js add <component-name> [--registry <url>] Install a component
710
+ node scripts/cli-remote.js list [--registry <url>] List all available components
711
+ node scripts/cli-remote.js css [--registry <url>] [--overwrite] Install/update CSS styles
712
+
713
+ Options:
714
+ --registry <url> Registry base URL (default: ${DEFAULT_REGISTRY_URL})
715
+ --overwrite, -f Overwrite existing files
716
+
717
+ Examples:
718
+ node scripts/cli-remote.js add button
719
+ node scripts/cli-remote.js add button --registry https://raw.githubusercontent.com/user/repo/main/registry
720
+ node scripts/cli-remote.js list
721
+ node scripts/cli-remote.js css
722
+ node scripts/cli-remote.js css --overwrite
723
+ `);
724
+ process.exit(0);
725
+ }
726
+
727
+ switch (command) {
728
+ case 'add':
729
+ if (!componentName) {
730
+ error('Please provide a component name');
731
+ console.log('Usage: node scripts/cli-remote.js add <component-name>');
732
+ process.exit(1);
733
+ }
734
+ await installComponent(componentName, options);
735
+ break;
736
+
737
+ case 'list':
738
+ await listComponents(registryUrl);
739
+ break;
740
+
741
+ case 'css':
742
+ case 'styles':
743
+ const config = loadComponentsConfig();
744
+ await installCSSStyles(config, registryUrl, options.overwrite);
745
+ break;
746
+
747
+ default:
748
+ error(`Unknown command: ${command}`);
749
+ console.log('Available commands: add, list, css');
750
+ process.exit(1);
751
+ }
752
+ }
753
+
754
+ main().catch(err => {
755
+ error(`Error: ${err.message}`);
756
+ process.exit(1);
757
+ });