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 +111 -0
- package/package.json +106 -0
- package/scripts/cli-remote.js +757 -0
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
|
+
});
|