weloop-kosign 1.1.0 → 1.1.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 +11 -0
- package/package.json +1 -1
- package/scripts/cli-remote.js +654 -57
package/scripts/cli-remote.js
CHANGED
|
@@ -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
|
|
@@ -33,21 +36,43 @@ const { execSync } = require('child_process');
|
|
|
33
36
|
// CONFIGURATION
|
|
34
37
|
// ============================================================================
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Determines the default registry URL to use.
|
|
41
|
+
*
|
|
42
|
+
* Priority:
|
|
43
|
+
* 1. Local registry directory (if running in the component library project)
|
|
44
|
+
* 2. Environment variable WELOOP_REGISTRY_URL
|
|
45
|
+
* 3. Default remote GitLab URL
|
|
46
|
+
*
|
|
47
|
+
* @returns {string} The registry URL or path
|
|
48
|
+
*/
|
|
36
49
|
function getDefaultRegistryUrl() {
|
|
37
50
|
const localRegistryPath = path.join(__dirname, '../registry');
|
|
51
|
+
|
|
52
|
+
// Check if we're running in the component library project itself
|
|
38
53
|
if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
|
|
39
54
|
return localRegistryPath;
|
|
40
55
|
}
|
|
56
|
+
|
|
57
|
+
// Fall back to environment variable or default remote URL
|
|
41
58
|
return process.env.WELOOP_REGISTRY_URL ||
|
|
42
59
|
'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
|
|
46
63
|
|
|
64
|
+
// Network timeout constants (in milliseconds)
|
|
65
|
+
const REQUEST_TIMEOUT = 15000;
|
|
66
|
+
const RETRY_DELAY = 1000;
|
|
67
|
+
|
|
47
68
|
// ============================================================================
|
|
48
|
-
// UTILITIES
|
|
69
|
+
// UTILITIES - Logging and File System Helpers
|
|
49
70
|
// ============================================================================
|
|
50
71
|
|
|
72
|
+
/**
|
|
73
|
+
* ANSI color codes for terminal output
|
|
74
|
+
* Makes the CLI output more readable with colored messages
|
|
75
|
+
*/
|
|
51
76
|
const colors = {
|
|
52
77
|
reset: '\x1b[0m',
|
|
53
78
|
green: '\x1b[32m',
|
|
@@ -57,52 +82,98 @@ const colors = {
|
|
|
57
82
|
cyan: '\x1b[36m',
|
|
58
83
|
};
|
|
59
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Logs a message with optional color formatting
|
|
87
|
+
* @param {string} message - The message to log
|
|
88
|
+
* @param {string} color - Color name from colors object
|
|
89
|
+
*/
|
|
60
90
|
function log(message, color = 'reset') {
|
|
61
91
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
62
92
|
}
|
|
63
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Logs an error message in red
|
|
96
|
+
*/
|
|
64
97
|
function error(message) {
|
|
65
98
|
log(`Error: ${message}`, 'red');
|
|
66
99
|
}
|
|
67
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Logs a success message in green
|
|
103
|
+
*/
|
|
68
104
|
function success(message) {
|
|
69
105
|
log(message, 'green');
|
|
70
106
|
}
|
|
71
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Logs an informational message in blue
|
|
110
|
+
*/
|
|
72
111
|
function info(message) {
|
|
73
112
|
log(message, 'blue');
|
|
74
113
|
}
|
|
75
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Logs a warning message in yellow
|
|
117
|
+
*/
|
|
76
118
|
function warn(message) {
|
|
77
119
|
log(`Warning: ${message}`, 'yellow');
|
|
78
120
|
}
|
|
79
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Creates a directory if it doesn't exist
|
|
124
|
+
* Uses recursive option to create parent directories as needed
|
|
125
|
+
* @param {string} dirPath - Path to the directory
|
|
126
|
+
*/
|
|
80
127
|
function ensureDirectoryExists(dirPath) {
|
|
81
128
|
if (!fs.existsSync(dirPath)) {
|
|
82
129
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
83
130
|
}
|
|
84
131
|
}
|
|
85
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Checks if a path is a local file path or a remote URL
|
|
135
|
+
* @param {string} pathOrUrl - Path or URL to check
|
|
136
|
+
* @returns {boolean} True if it's a local path, false if it's a URL
|
|
137
|
+
*/
|
|
86
138
|
function isLocalPath(pathOrUrl) {
|
|
87
139
|
return !pathOrUrl.startsWith('http://') && !pathOrUrl.startsWith('https://');
|
|
88
140
|
}
|
|
89
141
|
|
|
90
142
|
// ============================================================================
|
|
91
|
-
// PACKAGE MANAGEMENT
|
|
143
|
+
// PACKAGE MANAGEMENT - Detecting and Installing Dependencies
|
|
92
144
|
// ============================================================================
|
|
93
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Automatically detects which package manager the project is using
|
|
148
|
+
*
|
|
149
|
+
* Detection order:
|
|
150
|
+
* 1. Check for lock files (most reliable)
|
|
151
|
+
* 2. Check npm user agent (fallback)
|
|
152
|
+
* 3. Default to npm
|
|
153
|
+
*
|
|
154
|
+
* @returns {string} Package manager name: 'npm', 'yarn', or 'pnpm'
|
|
155
|
+
*/
|
|
94
156
|
function detectPackageManager() {
|
|
157
|
+
// Check for lock files first (most reliable indicator)
|
|
95
158
|
if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
|
|
96
159
|
if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
|
|
97
160
|
if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
|
|
98
161
|
|
|
162
|
+
// Fallback: check npm user agent (set by npm/yarn/pnpm when running)
|
|
99
163
|
const userAgent = process.env.npm_config_user_agent || '';
|
|
100
164
|
if (userAgent.includes('yarn')) return 'yarn';
|
|
101
165
|
if (userAgent.includes('pnpm')) return 'pnpm';
|
|
102
166
|
|
|
167
|
+
// Default to npm if we can't detect anything
|
|
103
168
|
return 'npm';
|
|
104
169
|
}
|
|
105
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Generates the install command for the given package manager
|
|
173
|
+
* @param {string} packageManager - 'npm', 'yarn', or 'pnpm'
|
|
174
|
+
* @param {string[]} packages - Array of package names to install
|
|
175
|
+
* @returns {string} The install command to run
|
|
176
|
+
*/
|
|
106
177
|
function getInstallCommand(packageManager, packages) {
|
|
107
178
|
const packagesStr = packages.join(' ');
|
|
108
179
|
switch (packageManager) {
|
|
@@ -113,36 +184,63 @@ function getInstallCommand(packageManager, packages) {
|
|
|
113
184
|
}
|
|
114
185
|
}
|
|
115
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Checks if a package is already installed in the project
|
|
189
|
+
* Searches both dependencies and devDependencies
|
|
190
|
+
* @param {string} packageName - Name of the package to check
|
|
191
|
+
* @returns {boolean} True if package is installed, false otherwise
|
|
192
|
+
*/
|
|
116
193
|
function checkPackageInstalled(packageName) {
|
|
117
194
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
118
195
|
if (!fs.existsSync(packageJsonPath)) return false;
|
|
119
196
|
|
|
120
197
|
try {
|
|
121
198
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
199
|
+
// Check both dependencies and devDependencies
|
|
122
200
|
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
123
201
|
return !!allDeps[packageName];
|
|
124
202
|
} catch (e) {
|
|
203
|
+
// If we can't read/parse package.json, assume package is not installed
|
|
125
204
|
return false;
|
|
126
205
|
}
|
|
127
206
|
}
|
|
128
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Filters out dependencies that are already installed
|
|
210
|
+
* Handles scoped packages (e.g., @radix-ui/react-button)
|
|
211
|
+
* @param {string[]} requiredDeps - List of required package names
|
|
212
|
+
* @returns {string[]} List of packages that need to be installed
|
|
213
|
+
*/
|
|
129
214
|
function getMissingDependencies(requiredDeps) {
|
|
130
215
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
131
|
-
if (!fs.existsSync(packageJsonPath))
|
|
216
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
217
|
+
// No package.json means we need to install everything
|
|
218
|
+
return requiredDeps;
|
|
219
|
+
}
|
|
132
220
|
|
|
133
221
|
try {
|
|
134
222
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
135
223
|
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
136
224
|
|
|
137
225
|
return requiredDeps.filter(dep => {
|
|
226
|
+
// For scoped packages like @radix-ui/react-button, also check @radix-ui
|
|
138
227
|
const depName = dep.split('/').slice(0, 2).join('/');
|
|
139
228
|
return !allDeps[dep] && !allDeps[depName];
|
|
140
229
|
});
|
|
141
230
|
} catch (e) {
|
|
231
|
+
// If we can't parse package.json, assume all deps are missing
|
|
142
232
|
return requiredDeps;
|
|
143
233
|
}
|
|
144
234
|
}
|
|
145
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Installs npm packages using the detected package manager
|
|
238
|
+
*
|
|
239
|
+
* For npm: Filters out ERESOLVE peer dependency warnings (common false positives)
|
|
240
|
+
* For yarn/pnpm: Uses standard execSync
|
|
241
|
+
*
|
|
242
|
+
* @param {string[]} packages - Array of package names to install
|
|
243
|
+
*/
|
|
146
244
|
async function installPackages(packages) {
|
|
147
245
|
if (packages.length === 0) return;
|
|
148
246
|
|
|
@@ -152,7 +250,8 @@ async function installPackages(packages) {
|
|
|
152
250
|
try {
|
|
153
251
|
const installCmd = getInstallCommand(packageManager, packages);
|
|
154
252
|
|
|
155
|
-
//
|
|
253
|
+
// npm has noisy ERESOLVE warnings that aren't real errors
|
|
254
|
+
// We filter these out while keeping actual error messages
|
|
156
255
|
if (packageManager === 'npm') {
|
|
157
256
|
const { spawn } = require('child_process');
|
|
158
257
|
const [cmd, ...args] = installCmd.split(' ');
|
|
@@ -167,7 +266,8 @@ async function installPackages(packages) {
|
|
|
167
266
|
let stderr = '';
|
|
168
267
|
proc.stderr.on('data', (data) => {
|
|
169
268
|
const output = data.toString();
|
|
170
|
-
// Filter out ERESOLVE warnings
|
|
269
|
+
// Filter out ERESOLVE peer dependency warnings (these are usually safe to ignore)
|
|
270
|
+
// but keep actual error messages visible to the user
|
|
171
271
|
if (!output.includes('ERESOLVE overriding peer dependency') &&
|
|
172
272
|
!output.includes('npm warn ERESOLVE')) {
|
|
173
273
|
process.stderr.write(data);
|
|
@@ -177,13 +277,14 @@ async function installPackages(packages) {
|
|
|
177
277
|
|
|
178
278
|
proc.on('close', (code) => {
|
|
179
279
|
if (code !== 0) {
|
|
180
|
-
//
|
|
280
|
+
// npm sometimes exits with non-zero code due to warnings, not real errors
|
|
281
|
+
// Check if there's an actual error message (not just ERESOLVE warnings)
|
|
181
282
|
const hasRealError = stderr.includes('npm error') &&
|
|
182
283
|
!stderr.match(/npm error.*ERESOLVE/);
|
|
183
284
|
if (hasRealError) {
|
|
184
285
|
reject(new Error(`Installation failed with code ${code}`));
|
|
185
286
|
} else {
|
|
186
|
-
// Installation succeeded despite warnings
|
|
287
|
+
// Installation succeeded despite warnings - this is common with peer deps
|
|
187
288
|
resolve();
|
|
188
289
|
}
|
|
189
290
|
} else {
|
|
@@ -206,13 +307,22 @@ async function installPackages(packages) {
|
|
|
206
307
|
}
|
|
207
308
|
|
|
208
309
|
// ============================================================================
|
|
209
|
-
// FILE OPERATIONS
|
|
310
|
+
// FILE OPERATIONS - Configuration and File Handling
|
|
210
311
|
// ============================================================================
|
|
211
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Creates a default components.json configuration file
|
|
315
|
+
*
|
|
316
|
+
* Auto-detects Next.js app directory structure and sets appropriate paths.
|
|
317
|
+
* This file stores project-specific settings like component paths and aliases.
|
|
318
|
+
*
|
|
319
|
+
* @returns {object} The default configuration object
|
|
320
|
+
*/
|
|
212
321
|
function createDefaultComponentsConfig() {
|
|
213
322
|
const configPath = path.join(process.cwd(), 'components.json');
|
|
214
323
|
|
|
215
|
-
// Detect
|
|
324
|
+
// Detect Next.js project structure
|
|
325
|
+
// App Router uses 'app/globals.css', Pages Router uses 'styles/globals.css'
|
|
216
326
|
const hasAppDir = fs.existsSync(path.join(process.cwd(), 'app'));
|
|
217
327
|
const cssPath = hasAppDir ? 'app/globals.css' : 'styles/globals.css';
|
|
218
328
|
|
|
@@ -245,6 +355,12 @@ function createDefaultComponentsConfig() {
|
|
|
245
355
|
return defaultConfig;
|
|
246
356
|
}
|
|
247
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Loads the components.json configuration file
|
|
360
|
+
* Creates a default one if it doesn't exist
|
|
361
|
+
*
|
|
362
|
+
* @returns {object} The configuration object
|
|
363
|
+
*/
|
|
248
364
|
function loadComponentsConfig() {
|
|
249
365
|
const configPath = path.join(process.cwd(), 'components.json');
|
|
250
366
|
|
|
@@ -257,10 +373,25 @@ function loadComponentsConfig() {
|
|
|
257
373
|
return JSON.parse(content);
|
|
258
374
|
}
|
|
259
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Fetches and parses a JSON file from a local path or remote URL
|
|
378
|
+
*
|
|
379
|
+
* Features:
|
|
380
|
+
* - Handles local files and remote URLs
|
|
381
|
+
* - Automatic retry on network errors
|
|
382
|
+
* - Follows HTTP redirects
|
|
383
|
+
* - Provides helpful error messages
|
|
384
|
+
*
|
|
385
|
+
* @param {string} urlOrPath - Local file path or remote URL
|
|
386
|
+
* @param {number} retries - Number of retry attempts (default: 3)
|
|
387
|
+
* @returns {Promise<object>} Parsed JSON object
|
|
388
|
+
*/
|
|
260
389
|
async function fetchJSON(urlOrPath, retries = 3) {
|
|
390
|
+
// Handle local file paths
|
|
261
391
|
if (isLocalPath(urlOrPath)) {
|
|
262
392
|
return new Promise((resolve, reject) => {
|
|
263
393
|
try {
|
|
394
|
+
// Resolve relative paths to absolute
|
|
264
395
|
const fullPath = path.isAbsolute(urlOrPath)
|
|
265
396
|
? urlOrPath
|
|
266
397
|
: path.join(process.cwd(), urlOrPath);
|
|
@@ -278,6 +409,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
278
409
|
});
|
|
279
410
|
}
|
|
280
411
|
|
|
412
|
+
// Handle remote URLs
|
|
281
413
|
return new Promise((resolve, reject) => {
|
|
282
414
|
const client = urlOrPath.startsWith('https') ? https : http;
|
|
283
415
|
let timeout;
|
|
@@ -287,10 +419,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
287
419
|
request = client.get(urlOrPath, (res) => {
|
|
288
420
|
clearTimeout(timeout);
|
|
289
421
|
|
|
422
|
+
// Handle HTTP redirects (301, 302)
|
|
290
423
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
291
424
|
return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
|
|
292
425
|
}
|
|
293
426
|
|
|
427
|
+
// Provide helpful error messages for common HTTP status codes
|
|
294
428
|
if (res.statusCode === 403) {
|
|
295
429
|
reject(new Error(`Access forbidden (403). Repository may be private. Make it public in GitLab/GitHub settings, or use --registry with a public URL.`));
|
|
296
430
|
return;
|
|
@@ -306,10 +440,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
306
440
|
return;
|
|
307
441
|
}
|
|
308
442
|
|
|
443
|
+
// Collect response data
|
|
309
444
|
let data = '';
|
|
310
445
|
res.on('data', (chunk) => { data += chunk; });
|
|
311
446
|
res.on('end', () => {
|
|
312
447
|
try {
|
|
448
|
+
// Check if we got HTML instead of JSON (common when repo is private)
|
|
313
449
|
if (data.trim().startsWith('<')) {
|
|
314
450
|
reject(new Error('Received HTML instead of JSON. Repository may be private or URL incorrect.'));
|
|
315
451
|
return;
|
|
@@ -321,36 +457,46 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
321
457
|
});
|
|
322
458
|
});
|
|
323
459
|
|
|
460
|
+
// Handle network errors with automatic retry
|
|
324
461
|
request.on('error', (err) => {
|
|
325
462
|
clearTimeout(timeout);
|
|
326
463
|
if (retries > 0 && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND')) {
|
|
327
464
|
info(`Connection error, retrying... (${retries} attempts left)`);
|
|
328
465
|
setTimeout(() => {
|
|
329
466
|
fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
|
|
330
|
-
},
|
|
467
|
+
}, RETRY_DELAY);
|
|
331
468
|
} else {
|
|
332
469
|
reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
|
|
333
470
|
}
|
|
334
471
|
});
|
|
335
472
|
|
|
473
|
+
// Set request timeout with retry logic
|
|
336
474
|
timeout = setTimeout(() => {
|
|
337
475
|
request.destroy();
|
|
338
476
|
if (retries > 0) {
|
|
339
477
|
info(`Request timeout, retrying... (${retries} attempts left)`);
|
|
340
478
|
setTimeout(() => {
|
|
341
479
|
fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
|
|
342
|
-
},
|
|
480
|
+
}, RETRY_DELAY);
|
|
343
481
|
} else {
|
|
344
482
|
reject(new Error('Request timeout after multiple attempts'));
|
|
345
483
|
}
|
|
346
|
-
},
|
|
484
|
+
}, REQUEST_TIMEOUT);
|
|
347
485
|
};
|
|
348
486
|
|
|
349
487
|
makeRequest();
|
|
350
488
|
});
|
|
351
489
|
}
|
|
352
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Fetches text content (like CSS) from a local path or remote URL
|
|
493
|
+
* Similar to fetchJSON but returns raw text instead of parsing JSON
|
|
494
|
+
*
|
|
495
|
+
* @param {string} urlOrPath - Local file path or remote URL
|
|
496
|
+
* @returns {Promise<string>} The file content as text
|
|
497
|
+
*/
|
|
353
498
|
async function fetchText(urlOrPath) {
|
|
499
|
+
// Handle local file paths
|
|
354
500
|
if (isLocalPath(urlOrPath)) {
|
|
355
501
|
if (!fs.existsSync(urlOrPath)) {
|
|
356
502
|
throw new Error(`File not found: ${urlOrPath}`);
|
|
@@ -358,15 +504,18 @@ async function fetchText(urlOrPath) {
|
|
|
358
504
|
return fs.readFileSync(urlOrPath, 'utf-8');
|
|
359
505
|
}
|
|
360
506
|
|
|
507
|
+
// Handle remote URLs
|
|
361
508
|
return new Promise((resolve, reject) => {
|
|
362
509
|
const client = urlOrPath.startsWith('https') ? https : http;
|
|
363
510
|
let data = '';
|
|
364
511
|
|
|
365
512
|
const req = client.get(urlOrPath, (res) => {
|
|
513
|
+
// Follow redirects
|
|
366
514
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
367
515
|
return fetchText(res.headers.location).then(resolve).catch(reject);
|
|
368
516
|
}
|
|
369
517
|
|
|
518
|
+
// Handle common HTTP errors
|
|
370
519
|
if (res.statusCode === 403) {
|
|
371
520
|
reject(new Error('Access forbidden - repository may be private'));
|
|
372
521
|
return;
|
|
@@ -382,8 +531,10 @@ async function fetchText(urlOrPath) {
|
|
|
382
531
|
return;
|
|
383
532
|
}
|
|
384
533
|
|
|
534
|
+
// Collect response data
|
|
385
535
|
res.on('data', (chunk) => { data += chunk; });
|
|
386
536
|
res.on('end', () => {
|
|
537
|
+
// Check if we got HTML instead of CSS (common when repo is private)
|
|
387
538
|
if (data.trim().startsWith('<')) {
|
|
388
539
|
reject(new Error('Received HTML instead of CSS'));
|
|
389
540
|
return;
|
|
@@ -392,42 +543,70 @@ async function fetchText(urlOrPath) {
|
|
|
392
543
|
});
|
|
393
544
|
});
|
|
394
545
|
|
|
546
|
+
// Handle network errors with automatic retry
|
|
395
547
|
req.on('error', (err) => {
|
|
396
548
|
if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
|
|
397
549
|
setTimeout(() => {
|
|
398
550
|
fetchText(urlOrPath).then(resolve).catch(reject);
|
|
399
|
-
},
|
|
551
|
+
}, RETRY_DELAY);
|
|
400
552
|
} else {
|
|
401
553
|
reject(err);
|
|
402
554
|
}
|
|
403
555
|
});
|
|
404
556
|
|
|
405
|
-
|
|
557
|
+
// Set request timeout
|
|
558
|
+
req.setTimeout(REQUEST_TIMEOUT, () => {
|
|
406
559
|
req.destroy();
|
|
407
560
|
reject(new Error('Request timeout'));
|
|
408
561
|
});
|
|
409
562
|
});
|
|
410
563
|
}
|
|
411
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Resolves the full path/URL for a registry file
|
|
567
|
+
* Handles both local paths and remote URLs
|
|
568
|
+
*
|
|
569
|
+
* @param {string} baseUrl - Base registry URL or path
|
|
570
|
+
* @param {string} fileName - Name of the file to resolve
|
|
571
|
+
* @returns {string} Full path or URL to the file
|
|
572
|
+
*/
|
|
412
573
|
function resolveRegistryPath(baseUrl, fileName) {
|
|
413
574
|
if (isLocalPath(baseUrl)) {
|
|
575
|
+
// Resolve local paths (absolute or relative)
|
|
414
576
|
const basePath = path.isAbsolute(baseUrl)
|
|
415
577
|
? baseUrl
|
|
416
578
|
: path.join(process.cwd(), baseUrl);
|
|
417
579
|
return path.join(basePath, fileName);
|
|
418
580
|
}
|
|
581
|
+
// For remote URLs, just append the filename
|
|
419
582
|
return `${baseUrl}/${fileName}`;
|
|
420
583
|
}
|
|
421
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Loads a component registry file from the registry
|
|
587
|
+
* Returns null if component not found (doesn't throw)
|
|
588
|
+
*
|
|
589
|
+
* @param {string} componentName - Name of the component
|
|
590
|
+
* @param {string} registryBaseUrl - Base URL/path of the registry
|
|
591
|
+
* @returns {Promise<object|null>} Component registry object or null if not found
|
|
592
|
+
*/
|
|
422
593
|
async function loadRegistry(componentName, registryBaseUrl) {
|
|
423
594
|
try {
|
|
424
595
|
const registryPath = resolveRegistryPath(registryBaseUrl, `${componentName}.json`);
|
|
425
596
|
return await fetchJSON(registryPath);
|
|
426
597
|
} catch (err) {
|
|
598
|
+
// Component not found - return null instead of throwing
|
|
427
599
|
return null;
|
|
428
600
|
}
|
|
429
601
|
}
|
|
430
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Loads the registry index file (lists all available components)
|
|
605
|
+
* Throws an error if index cannot be loaded
|
|
606
|
+
*
|
|
607
|
+
* @param {string} registryBaseUrl - Base URL/path of the registry
|
|
608
|
+
* @returns {Promise<object>} Index object containing list of components
|
|
609
|
+
*/
|
|
431
610
|
async function loadIndex(registryBaseUrl) {
|
|
432
611
|
const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
|
|
433
612
|
try {
|
|
@@ -439,31 +618,47 @@ async function loadIndex(registryBaseUrl) {
|
|
|
439
618
|
}
|
|
440
619
|
|
|
441
620
|
// ============================================================================
|
|
442
|
-
// CSS PROCESSING
|
|
621
|
+
// CSS PROCESSING - Style Installation and Formatting
|
|
443
622
|
// ============================================================================
|
|
444
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Checks if CSS content contains Weloop design system variables
|
|
626
|
+
* Used to detect if styles have already been installed
|
|
627
|
+
*
|
|
628
|
+
* @param {string} content - CSS content to check
|
|
629
|
+
* @returns {boolean} True if Weloop styles are present
|
|
630
|
+
*/
|
|
445
631
|
function hasWeloopStyles(content) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
content.includes('--WLDS-
|
|
449
|
-
content.includes('--
|
|
632
|
+
// Check for Weloop-specific CSS variable prefixes
|
|
633
|
+
return content.includes('--WLDS-PRM-') || // Primary colors
|
|
634
|
+
content.includes('--WLDS-RED-') || // Red colors
|
|
635
|
+
content.includes('--WLDS-NTL-') || // Neutral colors
|
|
636
|
+
content.includes('--system-100') || // System colors
|
|
450
637
|
content.includes('--system-200');
|
|
451
638
|
}
|
|
452
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Removes duplicate tw-animate-css imports and keeps only one
|
|
642
|
+
* Ensures the import appears right after tailwindcss import
|
|
643
|
+
*
|
|
644
|
+
* @param {string} cssContent - CSS content to clean
|
|
645
|
+
* @returns {string} CSS content with duplicates removed
|
|
646
|
+
*/
|
|
453
647
|
function removeDuplicateTwAnimateImports(cssContent) {
|
|
454
648
|
const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
|
|
455
649
|
const matches = cssContent.match(twAnimatePattern);
|
|
456
650
|
|
|
457
651
|
if (matches && matches.length > 1) {
|
|
458
|
-
// Remove all occurrences
|
|
652
|
+
// Remove all occurrences first
|
|
459
653
|
let cleaned = cssContent.replace(twAnimatePattern, '');
|
|
460
|
-
// Add it back once after tailwindcss import
|
|
654
|
+
// Add it back once, right after tailwindcss import if it exists
|
|
461
655
|
if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
|
|
462
656
|
cleaned = cleaned.replace(
|
|
463
657
|
/(@import\s+["']tailwindcss["'];?\s*\n?)/,
|
|
464
658
|
'$1@import "tw-animate-css";\n'
|
|
465
659
|
);
|
|
466
660
|
} else {
|
|
661
|
+
// If no tailwindcss import, add it at the beginning
|
|
467
662
|
cleaned = '@import "tw-animate-css";\n' + cleaned;
|
|
468
663
|
}
|
|
469
664
|
return cleaned;
|
|
@@ -472,11 +667,19 @@ function removeDuplicateTwAnimateImports(cssContent) {
|
|
|
472
667
|
return cssContent;
|
|
473
668
|
}
|
|
474
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Removes duplicate @custom-variant declarations
|
|
672
|
+
* Keeps only one instance at the top
|
|
673
|
+
*
|
|
674
|
+
* @param {string} cssContent - CSS content to clean
|
|
675
|
+
* @returns {string} CSS content with duplicates removed
|
|
676
|
+
*/
|
|
475
677
|
function removeDuplicateCustomVariant(cssContent) {
|
|
476
678
|
const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
|
|
477
679
|
const matches = cssContent.match(variantPattern);
|
|
478
680
|
|
|
479
681
|
if (matches && matches.length > 1) {
|
|
682
|
+
// Remove all occurrences and add back one at the top
|
|
480
683
|
const withoutVariants = cssContent.replace(variantPattern, '');
|
|
481
684
|
return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
|
|
482
685
|
}
|
|
@@ -484,11 +687,93 @@ function removeDuplicateCustomVariant(cssContent) {
|
|
|
484
687
|
return cssContent;
|
|
485
688
|
}
|
|
486
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Normalizes CSS format to ensure consistent structure
|
|
692
|
+
*
|
|
693
|
+
* Format order:
|
|
694
|
+
* 1. @import statements (tailwindcss, then tw-animate-css, then others)
|
|
695
|
+
* 2. @custom-variant declaration
|
|
696
|
+
* 3. Rest of the CSS content
|
|
697
|
+
*
|
|
698
|
+
* This ensures the CSS file always has the same structure regardless of
|
|
699
|
+
* how it was generated or merged.
|
|
700
|
+
*
|
|
701
|
+
* @param {string} cssContent - CSS content to normalize
|
|
702
|
+
* @returns {string} Normalized CSS content
|
|
703
|
+
*/
|
|
704
|
+
function normalizeCSSFormat(cssContent) {
|
|
705
|
+
// Extract @custom-variant declaration
|
|
706
|
+
const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
|
|
707
|
+
const variantMatch = cssContent.match(variantPattern);
|
|
708
|
+
const hasVariant = variantMatch && variantMatch.length > 0;
|
|
709
|
+
|
|
710
|
+
// Extract all @import statements
|
|
711
|
+
const importPattern = /@import\s+["'][^"']+["'];?\s*\n?/g;
|
|
712
|
+
const imports = cssContent.match(importPattern) || [];
|
|
713
|
+
|
|
714
|
+
// Separate imports by type for proper ordering
|
|
715
|
+
const tailwindImport = imports.find(imp => imp.includes('tailwindcss'));
|
|
716
|
+
const twAnimateImport = imports.find(imp => imp.includes('tw-animate-css'));
|
|
717
|
+
const otherImports = imports.filter(imp =>
|
|
718
|
+
!imp.includes('tailwindcss') && !imp.includes('tw-animate-css')
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Remove all imports and variant from content to get the rest
|
|
722
|
+
let content = cssContent
|
|
723
|
+
.replace(variantPattern, '')
|
|
724
|
+
.replace(importPattern, '')
|
|
725
|
+
.trim();
|
|
726
|
+
|
|
727
|
+
// Build normalized format in the correct order
|
|
728
|
+
let normalized = '';
|
|
729
|
+
|
|
730
|
+
// Step 1: Imports first (tailwindcss, then tw-animate-css, then others)
|
|
731
|
+
if (tailwindImport) {
|
|
732
|
+
normalized += tailwindImport.trim() + '\n';
|
|
733
|
+
}
|
|
734
|
+
if (twAnimateImport) {
|
|
735
|
+
normalized += twAnimateImport.trim() + '\n';
|
|
736
|
+
}
|
|
737
|
+
if (otherImports.length > 0) {
|
|
738
|
+
normalized += otherImports.join('') + '\n';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Step 2: @custom-variant second (if it exists)
|
|
742
|
+
if (hasVariant) {
|
|
743
|
+
if (normalized) {
|
|
744
|
+
normalized += '\n@custom-variant dark (&:is(.dark *));\n';
|
|
745
|
+
} else {
|
|
746
|
+
normalized += '@custom-variant dark (&:is(.dark *));\n';
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Step 3: Rest of content (theme variables, etc.)
|
|
751
|
+
if (normalized && content) {
|
|
752
|
+
normalized += '\n' + content;
|
|
753
|
+
} else if (content) {
|
|
754
|
+
normalized = content;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return normalized.trim() + (normalized ? '\n' : '');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Processes tw-animate-css import based on whether the package is installed
|
|
762
|
+
*
|
|
763
|
+
* - If package is NOT installed: Removes the import to prevent build errors
|
|
764
|
+
* - If package IS installed: Ensures import exists and removes duplicates
|
|
765
|
+
*
|
|
766
|
+
* @param {string} cssContent - CSS content to process
|
|
767
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
768
|
+
* @param {boolean} forceUpdate - Whether this is a forced update
|
|
769
|
+
* @returns {string} Processed CSS content
|
|
770
|
+
*/
|
|
487
771
|
function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
488
772
|
const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
|
|
489
773
|
let processed = cssContent;
|
|
490
774
|
|
|
491
775
|
if (!hasTwAnimate) {
|
|
776
|
+
// Package not installed - remove import to prevent build errors
|
|
492
777
|
processed = cssContent.replace(twAnimatePattern, '');
|
|
493
778
|
if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
|
|
494
779
|
if (forceUpdate) {
|
|
@@ -502,10 +787,10 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
502
787
|
}
|
|
503
788
|
}
|
|
504
789
|
} else {
|
|
505
|
-
//
|
|
790
|
+
// Package is installed - ensure import exists and remove duplicates
|
|
506
791
|
processed = removeDuplicateTwAnimateImports(processed);
|
|
507
792
|
|
|
508
|
-
//
|
|
793
|
+
// Add import if it doesn't exist (right after tailwindcss if present)
|
|
509
794
|
if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
510
795
|
if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
|
|
511
796
|
processed = processed.replace(
|
|
@@ -521,10 +806,25 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
521
806
|
return processed;
|
|
522
807
|
}
|
|
523
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Removes tailwindcss import statements from CSS
|
|
811
|
+
* Used when merging styles to avoid duplicates
|
|
812
|
+
*
|
|
813
|
+
* @param {string} cssContent - CSS content
|
|
814
|
+
* @returns {string} CSS content without tailwindcss imports
|
|
815
|
+
*/
|
|
524
816
|
function removeTailwindImport(cssContent) {
|
|
525
817
|
return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
|
|
526
818
|
}
|
|
527
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Ensures tw-animate-css import exists if the package is installed
|
|
822
|
+
* Adds it right after tailwindcss import if present
|
|
823
|
+
*
|
|
824
|
+
* @param {string} cssContent - CSS content
|
|
825
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
826
|
+
* @returns {string} CSS content with tw-animate-css import ensured
|
|
827
|
+
*/
|
|
528
828
|
function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
529
829
|
if (!hasTwAnimate) {
|
|
530
830
|
return cssContent;
|
|
@@ -538,7 +838,7 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
538
838
|
return cleaned;
|
|
539
839
|
}
|
|
540
840
|
|
|
541
|
-
// Add import after tailwindcss if it exists
|
|
841
|
+
// Add import after tailwindcss if it exists, otherwise at the beginning
|
|
542
842
|
if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
|
|
543
843
|
return cleaned.replace(
|
|
544
844
|
/(@import\s+["']tailwindcss["'];?\s*\n?)/,
|
|
@@ -549,21 +849,36 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
549
849
|
return '@import "tw-animate-css";\n' + cleaned;
|
|
550
850
|
}
|
|
551
851
|
|
|
852
|
+
/**
|
|
853
|
+
* Merges Weloop styles with existing CSS that has Tailwind imports
|
|
854
|
+
*
|
|
855
|
+
* Strategy:
|
|
856
|
+
* - Finds the tailwindcss import in existing CSS
|
|
857
|
+
* - Inserts Weloop styles right after it
|
|
858
|
+
* - Handles tw-animate-css import to avoid duplicates
|
|
859
|
+
*
|
|
860
|
+
* @param {string} existing - Existing CSS content
|
|
861
|
+
* @param {string} weloopStyles - Weloop styles to merge in
|
|
862
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
863
|
+
* @returns {string} Merged CSS content
|
|
864
|
+
*/
|
|
552
865
|
function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
|
|
553
866
|
const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
|
|
554
867
|
if (!tailwindMatch) {
|
|
868
|
+
// No tailwindcss import found - just prepend Weloop styles
|
|
555
869
|
return weloopStyles + '\n\n' + existing;
|
|
556
870
|
}
|
|
557
871
|
|
|
872
|
+
// Split existing CSS around the tailwindcss import
|
|
558
873
|
const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
|
|
559
874
|
const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
|
|
560
875
|
|
|
561
|
-
// Check if tw-animate-css import already exists
|
|
876
|
+
// Check if tw-animate-css import already exists
|
|
562
877
|
const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
563
878
|
const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
564
879
|
|
|
565
|
-
// If
|
|
566
|
-
//
|
|
880
|
+
// If existing file already has tw-animate import, remove it from Weloop styles
|
|
881
|
+
// to avoid duplicating it in the merge output
|
|
567
882
|
let weloopStylesCleaned = weloopStyles;
|
|
568
883
|
if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
|
|
569
884
|
weloopStylesCleaned = weloopStyles.replace(
|
|
@@ -572,44 +887,75 @@ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
|
|
|
572
887
|
);
|
|
573
888
|
}
|
|
574
889
|
|
|
890
|
+
// Add tw-animate import if needed (package installed but import missing)
|
|
575
891
|
let importsToAdd = '';
|
|
576
892
|
if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
|
|
577
893
|
importsToAdd = '@import "tw-animate-css";\n';
|
|
578
894
|
}
|
|
579
895
|
|
|
896
|
+
// Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
|
|
580
897
|
return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
|
|
581
898
|
}
|
|
582
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Replaces existing Weloop styles with new ones
|
|
902
|
+
*
|
|
903
|
+
* Finds where Weloop styles start (usually @theme inline or :root)
|
|
904
|
+
* and replaces everything from there with the new styles.
|
|
905
|
+
* Preserves imports that come before the Weloop styles.
|
|
906
|
+
*
|
|
907
|
+
* @param {string} existing - Existing CSS content
|
|
908
|
+
* @param {string} newStyles - New Weloop styles to replace with
|
|
909
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
910
|
+
* @returns {string} CSS content with Weloop styles replaced
|
|
911
|
+
*/
|
|
583
912
|
function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
|
|
584
913
|
const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
|
|
585
914
|
const imports = existing.match(importPattern) || [];
|
|
586
915
|
const importsText = imports.join('');
|
|
587
916
|
|
|
917
|
+
// Find where Weloop styles start (look for @theme inline or :root)
|
|
588
918
|
const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
|
|
589
919
|
const weloopStartMatch = existing.search(weloopStartPattern);
|
|
590
920
|
|
|
591
921
|
if (weloopStartMatch === -1) {
|
|
922
|
+
// No existing Weloop styles found - just append
|
|
592
923
|
return existing + '\n\n' + newStyles;
|
|
593
924
|
}
|
|
594
925
|
|
|
926
|
+
// Extract content before Weloop styles
|
|
595
927
|
let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
|
|
928
|
+
// Keep non-Weloop imports (filter out tw-animate if package not installed)
|
|
596
929
|
const nonWeloopImports = importsText.split('\n').filter(imp =>
|
|
597
930
|
!imp.includes('tw-animate-css') || hasTwAnimate
|
|
598
931
|
).join('\n');
|
|
932
|
+
// Remove all imports from contentBeforeWeloop (we'll add them back)
|
|
599
933
|
contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
|
|
600
934
|
|
|
935
|
+
// Return: preserved imports + content before Weloop + new Weloop styles
|
|
601
936
|
return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
|
|
602
937
|
}
|
|
603
938
|
|
|
939
|
+
/**
|
|
940
|
+
* Fetches the CSS file from the registry
|
|
941
|
+
*
|
|
942
|
+
* For local paths: Looks for app/globals.css relative to registry directory
|
|
943
|
+
* For remote URLs: Constructs URL by replacing /registry with /app/globals.css
|
|
944
|
+
*
|
|
945
|
+
* @param {string} registryUrl - Registry base URL or path
|
|
946
|
+
* @returns {Promise<string>} CSS file content
|
|
947
|
+
*/
|
|
604
948
|
async function fetchCSSFromRegistry(registryUrl) {
|
|
605
949
|
let sourceCssPath;
|
|
606
950
|
|
|
607
951
|
if (isLocalPath(registryUrl)) {
|
|
952
|
+
// Local path: find app/globals.css relative to registry directory
|
|
608
953
|
const basePath = path.isAbsolute(registryUrl)
|
|
609
954
|
? path.dirname(registryUrl)
|
|
610
955
|
: path.join(process.cwd(), path.dirname(registryUrl));
|
|
611
956
|
sourceCssPath = path.join(basePath, 'app', 'globals.css');
|
|
612
957
|
} else {
|
|
958
|
+
// Remote URL: replace /registry with /app/globals.css
|
|
613
959
|
const baseUrl = registryUrl.replace('/registry', '');
|
|
614
960
|
sourceCssPath = `${baseUrl}/app/globals.css`;
|
|
615
961
|
}
|
|
@@ -617,17 +963,31 @@ async function fetchCSSFromRegistry(registryUrl) {
|
|
|
617
963
|
return await fetchText(sourceCssPath);
|
|
618
964
|
}
|
|
619
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Installs or updates CSS styles from the registry
|
|
968
|
+
*
|
|
969
|
+
* Handles three scenarios:
|
|
970
|
+
* 1. --overwrite: Replaces entire file
|
|
971
|
+
* 2. Normal update: Intelligently merges with existing styles
|
|
972
|
+
* 3. New file: Creates file with Weloop styles
|
|
973
|
+
*
|
|
974
|
+
* @param {object} config - Components configuration object
|
|
975
|
+
* @param {string} registryUrl - Registry base URL or path
|
|
976
|
+
* @param {boolean} forceUpdate - Whether to overwrite existing file
|
|
977
|
+
* @param {boolean} silent - Whether to suppress output messages
|
|
978
|
+
*/
|
|
620
979
|
async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
|
|
621
980
|
const cssPath = config.tailwind?.css || 'app/globals.css';
|
|
622
981
|
const fullCssPath = path.join(process.cwd(), cssPath);
|
|
623
982
|
|
|
624
|
-
// Check if Weloop styles already exist
|
|
983
|
+
// Check if Weloop styles already exist in the file
|
|
625
984
|
let hasWeloopStylesInFile = false;
|
|
626
985
|
if (fs.existsSync(fullCssPath)) {
|
|
627
986
|
const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
|
|
628
987
|
hasWeloopStylesInFile = hasWeloopStyles(existingContent);
|
|
629
988
|
|
|
630
|
-
// If styles already exist and not forcing update, skip silently
|
|
989
|
+
// If styles already exist and we're not forcing update, skip silently
|
|
990
|
+
// This prevents unnecessary updates when installing components
|
|
631
991
|
if (hasWeloopStylesInFile && !forceUpdate && silent) {
|
|
632
992
|
return;
|
|
633
993
|
}
|
|
@@ -644,10 +1004,13 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
644
1004
|
const hasTwAnimate = checkPackageInstalled('tw-animate-css');
|
|
645
1005
|
let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
|
|
646
1006
|
|
|
647
|
-
// Handle file installation
|
|
1007
|
+
// Handle file installation based on mode and existing file state
|
|
648
1008
|
if (forceUpdate && fs.existsSync(fullCssPath)) {
|
|
649
|
-
// --overwrite
|
|
650
|
-
|
|
1009
|
+
// Mode 1: --overwrite flag - Replace entire file with fresh styles
|
|
1010
|
+
let normalized = normalizeCSSFormat(processedCssContent);
|
|
1011
|
+
normalized = removeDuplicateTwAnimateImports(normalized);
|
|
1012
|
+
normalized = removeDuplicateCustomVariant(normalized);
|
|
1013
|
+
fs.writeFileSync(fullCssPath, normalized);
|
|
651
1014
|
if (!silent) {
|
|
652
1015
|
success(`Overwritten ${cssPath} with Weloop styles`);
|
|
653
1016
|
if (hasTwAnimate) {
|
|
@@ -655,34 +1018,36 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
655
1018
|
}
|
|
656
1019
|
}
|
|
657
1020
|
} else if (fs.existsSync(fullCssPath)) {
|
|
658
|
-
//
|
|
1021
|
+
// Mode 2: Normal update - Intelligently merge with existing styles
|
|
659
1022
|
const existing = fs.readFileSync(fullCssPath, 'utf-8');
|
|
660
1023
|
const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
|
|
661
1024
|
existing.includes('@tailwind base');
|
|
662
1025
|
|
|
663
1026
|
if (hasWeloopStylesInFile) {
|
|
664
|
-
//
|
|
1027
|
+
// Case 2a: Weloop styles already exist - replace them with updated versions
|
|
665
1028
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
666
1029
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
667
1030
|
let finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
|
|
668
1031
|
finalContent = removeDuplicateTwAnimateImports(finalContent);
|
|
669
1032
|
finalContent = removeDuplicateCustomVariant(finalContent);
|
|
1033
|
+
finalContent = normalizeCSSFormat(finalContent);
|
|
670
1034
|
|
|
671
|
-
// Only
|
|
1035
|
+
// Only write if content actually changed (prevents unnecessary file updates)
|
|
672
1036
|
if (finalContent !== existing) {
|
|
673
1037
|
fs.writeFileSync(fullCssPath, finalContent);
|
|
674
1038
|
if (!silent) {
|
|
675
1039
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
676
1040
|
}
|
|
677
1041
|
}
|
|
678
|
-
// If no changes, silently skip
|
|
1042
|
+
// If no changes, silently skip (file is already up to date)
|
|
679
1043
|
} else if (hasTailwindImport) {
|
|
680
|
-
//
|
|
1044
|
+
// Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
|
|
681
1045
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
682
1046
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
683
1047
|
let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
|
|
684
1048
|
merged = removeDuplicateTwAnimateImports(merged);
|
|
685
1049
|
merged = removeDuplicateCustomVariant(merged);
|
|
1050
|
+
merged = normalizeCSSFormat(merged);
|
|
686
1051
|
fs.writeFileSync(fullCssPath, merged);
|
|
687
1052
|
if (!silent) {
|
|
688
1053
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
@@ -692,9 +1057,10 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
692
1057
|
info(` Your existing styles are preserved`);
|
|
693
1058
|
}
|
|
694
1059
|
} else {
|
|
695
|
-
// No Tailwind imports
|
|
1060
|
+
// Case 2c: No Tailwind imports - prepend Weloop styles to existing content
|
|
696
1061
|
let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
|
|
697
1062
|
|
|
1063
|
+
// Ensure tw-animate import exists if package is installed
|
|
698
1064
|
if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
699
1065
|
if (finalCssContent.includes('@import "tailwindcss"')) {
|
|
700
1066
|
finalCssContent = finalCssContent.replace(
|
|
@@ -705,7 +1071,9 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
705
1071
|
finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
|
|
706
1072
|
}
|
|
707
1073
|
}
|
|
708
|
-
|
|
1074
|
+
let combined = finalCssContent + '\n\n' + existing;
|
|
1075
|
+
combined = normalizeCSSFormat(combined);
|
|
1076
|
+
fs.writeFileSync(fullCssPath, combined);
|
|
709
1077
|
if (!silent) {
|
|
710
1078
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
711
1079
|
if (hasTwAnimate) {
|
|
@@ -714,8 +1082,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
714
1082
|
}
|
|
715
1083
|
}
|
|
716
1084
|
} else {
|
|
717
|
-
//
|
|
718
|
-
|
|
1085
|
+
// Mode 3: File doesn't exist - create new file with Weloop styles
|
|
1086
|
+
let normalized = normalizeCSSFormat(processedCssContent);
|
|
1087
|
+
normalized = removeDuplicateTwAnimateImports(normalized);
|
|
1088
|
+
normalized = removeDuplicateCustomVariant(normalized);
|
|
1089
|
+
fs.writeFileSync(fullCssPath, normalized);
|
|
719
1090
|
if (!silent) {
|
|
720
1091
|
success(`Created ${cssPath} with Weloop styles`);
|
|
721
1092
|
if (hasTwAnimate) {
|
|
@@ -735,13 +1106,22 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
735
1106
|
}
|
|
736
1107
|
|
|
737
1108
|
// ============================================================================
|
|
738
|
-
// COMPONENT INSTALLATION
|
|
1109
|
+
// COMPONENT INSTALLATION - Installing Components from Registry
|
|
739
1110
|
// ============================================================================
|
|
740
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* Creates the utils.ts file if it doesn't exist
|
|
1114
|
+
*
|
|
1115
|
+
* This file contains the 'cn' utility function used by all components
|
|
1116
|
+
* for merging Tailwind classes. It's required for components to work.
|
|
1117
|
+
*
|
|
1118
|
+
* @param {string} utilsPath - Path where utils.ts should be created
|
|
1119
|
+
*/
|
|
741
1120
|
function createUtilsFile(utilsPath) {
|
|
742
1121
|
if (fs.existsSync(utilsPath)) return;
|
|
743
1122
|
|
|
744
1123
|
// Silently create utils file (matching shadcn/ui behavior)
|
|
1124
|
+
// Don't overwrite if user has customized it
|
|
745
1125
|
ensureDirectoryExists(path.dirname(utilsPath));
|
|
746
1126
|
|
|
747
1127
|
const utilsContent = `import { clsx, type ClassValue } from "clsx"
|
|
@@ -754,10 +1134,27 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
754
1134
|
fs.writeFileSync(utilsPath, utilsContent);
|
|
755
1135
|
}
|
|
756
1136
|
|
|
1137
|
+
/**
|
|
1138
|
+
* Installs a component from the registry
|
|
1139
|
+
*
|
|
1140
|
+
* Installation process:
|
|
1141
|
+
* 1. Install base dependencies (clsx, tailwind-merge)
|
|
1142
|
+
* 2. Install tw-animate-css for animations
|
|
1143
|
+
* 3. Install/update CSS styles (silently if already installed)
|
|
1144
|
+
* 4. Install component dependencies (recursive)
|
|
1145
|
+
* 5. Install component files
|
|
1146
|
+
* 6. Create utils.ts if needed
|
|
1147
|
+
* 7. Install npm dependencies
|
|
1148
|
+
*
|
|
1149
|
+
* @param {string} componentName - Name of the component to install
|
|
1150
|
+
* @param {object} options - Installation options
|
|
1151
|
+
* @param {boolean} options.overwrite - Whether to overwrite existing files
|
|
1152
|
+
* @param {string} options.registryUrl - Registry URL or path
|
|
1153
|
+
*/
|
|
757
1154
|
async function installComponent(componentName, options = {}) {
|
|
758
1155
|
const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
|
|
759
1156
|
|
|
760
|
-
// Install base dependencies
|
|
1157
|
+
// Step 1: Install base dependencies (required for utils.ts to work)
|
|
761
1158
|
const baseDeps = [];
|
|
762
1159
|
if (!checkPackageInstalled('clsx')) {
|
|
763
1160
|
baseDeps.push('clsx');
|
|
@@ -770,19 +1167,20 @@ async function installComponent(componentName, options = {}) {
|
|
|
770
1167
|
await installPackages(baseDeps);
|
|
771
1168
|
}
|
|
772
1169
|
|
|
773
|
-
// Load or create components.json
|
|
1170
|
+
// Step 2: Load or create components.json configuration
|
|
774
1171
|
const config = loadComponentsConfig();
|
|
775
1172
|
|
|
776
|
-
// Install tw-animate-css automatically (required for animations)
|
|
1173
|
+
// Step 3: Install tw-animate-css automatically (required for component animations)
|
|
777
1174
|
if (!checkPackageInstalled('tw-animate-css')) {
|
|
778
1175
|
info('Installing tw-animate-css for animations...');
|
|
779
1176
|
await installPackages(['tw-animate-css']);
|
|
780
1177
|
}
|
|
781
1178
|
|
|
782
|
-
// Install CSS styles early (before component installation)
|
|
1179
|
+
// Step 4: Install CSS styles early (before component installation)
|
|
1180
|
+
// Silent mode prevents output when styles are already installed
|
|
783
1181
|
await installCSSStyles(config, registryUrl, false, true);
|
|
784
1182
|
|
|
785
|
-
//
|
|
1183
|
+
// Step 5: Resolve paths from components.json configuration
|
|
786
1184
|
const uiAlias = config.aliases?.ui || '@/components/ui';
|
|
787
1185
|
const utilsAlias = config.aliases?.utils || '@/lib/utils';
|
|
788
1186
|
|
|
@@ -793,30 +1191,34 @@ async function installComponent(componentName, options = {}) {
|
|
|
793
1191
|
}
|
|
794
1192
|
utilsPath = path.join(process.cwd(), utilsPath);
|
|
795
1193
|
|
|
796
|
-
// Load component registry
|
|
1194
|
+
// Step 6: Load component registry from remote or local source
|
|
797
1195
|
const registry = await loadRegistry(componentName, registryUrl);
|
|
798
1196
|
|
|
799
1197
|
if (!registry) {
|
|
800
1198
|
error(`Component "${componentName}" not found in registry.`);
|
|
801
1199
|
info('Available components:');
|
|
802
1200
|
try {
|
|
1201
|
+
// Show available components to help user
|
|
803
1202
|
const index = await loadIndex(registryUrl);
|
|
804
1203
|
index.registry.forEach(comp => {
|
|
805
1204
|
console.log(` - ${comp.name}`);
|
|
806
1205
|
});
|
|
807
1206
|
} catch (e) {
|
|
808
|
-
// Ignore if index fails
|
|
1207
|
+
// Ignore if index fails - we already showed the error
|
|
809
1208
|
}
|
|
810
1209
|
process.exit(1);
|
|
811
1210
|
}
|
|
812
1211
|
|
|
813
|
-
// Check if component already exists
|
|
1212
|
+
// Step 7: Check if component already exists
|
|
1213
|
+
// Silently skip if exists (matching shadcn/ui behavior)
|
|
1214
|
+
// Only install if overwrite flag is set
|
|
814
1215
|
const componentPath = path.join(componentsDir, `${componentName}.tsx`);
|
|
815
1216
|
if (fs.existsSync(componentPath) && !overwrite) {
|
|
816
1217
|
return;
|
|
817
1218
|
}
|
|
818
1219
|
|
|
819
|
-
// Install
|
|
1220
|
+
// Step 8: Install component dependencies first (recursive)
|
|
1221
|
+
// Some components depend on other components (e.g., button-group depends on button)
|
|
820
1222
|
if (registry.registryDependencies && registry.registryDependencies.length > 0) {
|
|
821
1223
|
info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
|
|
822
1224
|
for (const dep of registry.registryDependencies) {
|
|
@@ -824,7 +1226,8 @@ async function installComponent(componentName, options = {}) {
|
|
|
824
1226
|
}
|
|
825
1227
|
}
|
|
826
1228
|
|
|
827
|
-
// Install component files
|
|
1229
|
+
// Step 9: Install component files
|
|
1230
|
+
// Components can have multiple files (e.g., component + types + styles)
|
|
828
1231
|
for (const file of registry.files) {
|
|
829
1232
|
const filePath = path.join(process.cwd(), file.path);
|
|
830
1233
|
ensureDirectoryExists(path.dirname(filePath));
|
|
@@ -832,10 +1235,11 @@ async function installComponent(componentName, options = {}) {
|
|
|
832
1235
|
success(`Installed: ${file.path}`);
|
|
833
1236
|
}
|
|
834
1237
|
|
|
835
|
-
// Create utils.ts if
|
|
1238
|
+
// Step 10: Create utils.ts if it doesn't exist
|
|
1239
|
+
// This is required for all components to work
|
|
836
1240
|
createUtilsFile(utilsPath);
|
|
837
1241
|
|
|
838
|
-
// Install npm dependencies
|
|
1242
|
+
// Step 11: Install npm dependencies required by the component
|
|
839
1243
|
if (registry.dependencies && registry.dependencies.length > 0) {
|
|
840
1244
|
const missingDeps = getMissingDependencies(registry.dependencies);
|
|
841
1245
|
if (missingDeps.length > 0) {
|
|
@@ -849,15 +1253,184 @@ async function installComponent(componentName, options = {}) {
|
|
|
849
1253
|
}
|
|
850
1254
|
|
|
851
1255
|
// ============================================================================
|
|
852
|
-
//
|
|
1256
|
+
// INIT COMMAND - Project Initialization
|
|
853
1257
|
// ============================================================================
|
|
854
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* Detects the project type (Next.js or Vite)
|
|
1261
|
+
* @returns {string} 'nextjs' or 'vite' or null
|
|
1262
|
+
*/
|
|
1263
|
+
function detectProjectType() {
|
|
1264
|
+
// Check for Next.js
|
|
1265
|
+
if (fs.existsSync(path.join(process.cwd(), 'next.config.js')) ||
|
|
1266
|
+
fs.existsSync(path.join(process.cwd(), 'next.config.mjs')) ||
|
|
1267
|
+
fs.existsSync(path.join(process.cwd(), 'next.config.ts')) ||
|
|
1268
|
+
fs.existsSync(path.join(process.cwd(), 'app')) ||
|
|
1269
|
+
fs.existsSync(path.join(process.cwd(), 'pages'))) {
|
|
1270
|
+
return 'nextjs';
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Check for Vite
|
|
1274
|
+
if (fs.existsSync(path.join(process.cwd(), 'vite.config.js')) ||
|
|
1275
|
+
fs.existsSync(path.join(process.cwd(), 'vite.config.ts')) ||
|
|
1276
|
+
fs.existsSync(path.join(process.cwd(), 'vite.config.mjs'))) {
|
|
1277
|
+
return 'vite';
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Prompts user for configuration using readline
|
|
1285
|
+
* @param {string} question - Question to ask
|
|
1286
|
+
* @param {string} defaultValue - Default value
|
|
1287
|
+
* @returns {Promise<string>} User input
|
|
1288
|
+
*/
|
|
1289
|
+
function prompt(question, defaultValue = '') {
|
|
1290
|
+
return new Promise((resolve) => {
|
|
1291
|
+
const readline = require('readline');
|
|
1292
|
+
const rl = readline.createInterface({
|
|
1293
|
+
input: process.stdin,
|
|
1294
|
+
output: process.stdout
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
const promptText = defaultValue
|
|
1298
|
+
? `${question} › ${defaultValue} `
|
|
1299
|
+
: `${question} › `;
|
|
1300
|
+
|
|
1301
|
+
rl.question(promptText, (answer) => {
|
|
1302
|
+
rl.close();
|
|
1303
|
+
resolve(answer.trim() || defaultValue);
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Initializes the project with components.json configuration
|
|
1310
|
+
* Similar to shadcn/ui init command
|
|
1311
|
+
*/
|
|
1312
|
+
async function initProject() {
|
|
1313
|
+
info('Initializing project...\n');
|
|
1314
|
+
|
|
1315
|
+
// Check if components.json already exists
|
|
1316
|
+
const configPath = path.join(process.cwd(), 'components.json');
|
|
1317
|
+
if (fs.existsSync(configPath)) {
|
|
1318
|
+
warn('components.json already exists.');
|
|
1319
|
+
const overwrite = await prompt('Do you want to overwrite it? (y/N)', 'N');
|
|
1320
|
+
if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
|
|
1321
|
+
info('Skipping initialization.');
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Detect project type
|
|
1327
|
+
const projectType = detectProjectType();
|
|
1328
|
+
let detectedType = projectType || 'nextjs';
|
|
1329
|
+
|
|
1330
|
+
if (!projectType) {
|
|
1331
|
+
info('Could not detect project type. Defaulting to Next.js.');
|
|
1332
|
+
} else {
|
|
1333
|
+
info(`Detected ${projectType === 'nextjs' ? 'Next.js' : 'Vite'} project.`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Ask for project type if not detected or user wants to override
|
|
1337
|
+
const useDetected = projectType
|
|
1338
|
+
? await prompt(`Which project type? (Next.js/Vite)`, detectedType === 'nextjs' ? 'Next.js' : 'Vite')
|
|
1339
|
+
: await prompt(`Which project type? (Next.js/Vite)`, 'Next.js');
|
|
1340
|
+
|
|
1341
|
+
const isNextjs = useDetected.toLowerCase().includes('next');
|
|
1342
|
+
const isVite = useDetected.toLowerCase().includes('vite');
|
|
1343
|
+
|
|
1344
|
+
// Detect CSS path
|
|
1345
|
+
let cssPath = 'app/globals.css';
|
|
1346
|
+
if (isVite) {
|
|
1347
|
+
cssPath = 'src/index.css';
|
|
1348
|
+
} else if (fs.existsSync(path.join(process.cwd(), 'app', 'globals.css'))) {
|
|
1349
|
+
cssPath = 'app/globals.css';
|
|
1350
|
+
} else if (fs.existsSync(path.join(process.cwd(), 'styles', 'globals.css'))) {
|
|
1351
|
+
cssPath = 'styles/globals.css';
|
|
1352
|
+
} else {
|
|
1353
|
+
cssPath = await prompt('Where is your global CSS file?', cssPath);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Ask for base color
|
|
1357
|
+
const baseColor = await prompt('Which color would you like to use as base color?', 'neutral');
|
|
1358
|
+
|
|
1359
|
+
// Ask for style
|
|
1360
|
+
const style = await prompt('Which style would you like to use?', 'blue');
|
|
1361
|
+
|
|
1362
|
+
// Create components.json
|
|
1363
|
+
const config = {
|
|
1364
|
+
$schema: 'https://weloop.kosign.dev/schema.json',
|
|
1365
|
+
style: style,
|
|
1366
|
+
rsc: isNextjs,
|
|
1367
|
+
tsx: true,
|
|
1368
|
+
tailwind: {
|
|
1369
|
+
config: 'tailwind.config.ts',
|
|
1370
|
+
css: cssPath,
|
|
1371
|
+
baseColor: baseColor,
|
|
1372
|
+
cssVariables: true,
|
|
1373
|
+
prefix: ''
|
|
1374
|
+
},
|
|
1375
|
+
iconLibrary: 'lucide',
|
|
1376
|
+
aliases: {
|
|
1377
|
+
components: '@/components',
|
|
1378
|
+
utils: '@/lib/utils',
|
|
1379
|
+
ui: '@/components/ui',
|
|
1380
|
+
lib: '@/lib',
|
|
1381
|
+
hooks: '@/hooks'
|
|
1382
|
+
},
|
|
1383
|
+
registry: 'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry/index.json'
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1387
|
+
success('Created components.json\n');
|
|
1388
|
+
|
|
1389
|
+
// Install base dependencies
|
|
1390
|
+
info('Installing base dependencies...');
|
|
1391
|
+
const baseDeps = ['clsx', 'tailwind-merge'];
|
|
1392
|
+
const missingBaseDeps = baseDeps.filter(dep => !checkPackageInstalled(dep));
|
|
1393
|
+
|
|
1394
|
+
if (missingBaseDeps.length > 0) {
|
|
1395
|
+
await installPackages(missingBaseDeps);
|
|
1396
|
+
} else {
|
|
1397
|
+
info('Base dependencies already installed.');
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Install tw-animate-css
|
|
1401
|
+
if (!checkPackageInstalled('tw-animate-css')) {
|
|
1402
|
+
info('Installing tw-animate-css for animations...');
|
|
1403
|
+
await installPackages(['tw-animate-css']);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Install CSS styles
|
|
1407
|
+
info('\nInstalling CSS styles...');
|
|
1408
|
+
await installCSSStyles(config, DEFAULT_REGISTRY_URL, false, false);
|
|
1409
|
+
|
|
1410
|
+
success('\n✅ Project initialized successfully!');
|
|
1411
|
+
info('\nYou can now add components using:');
|
|
1412
|
+
info(' npx weloop-kosign@latest add <component-name>');
|
|
1413
|
+
info(' pnpm dlx weloop-kosign@latest add <component-name>');
|
|
1414
|
+
info(' yarn weloop-kosign@latest add <component-name>\n');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// ============================================================================
|
|
1418
|
+
// COMMANDS - CLI Command Handlers
|
|
1419
|
+
// ============================================================================
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Lists all available components from the registry
|
|
1423
|
+
* Shows component names and their dependencies
|
|
1424
|
+
*
|
|
1425
|
+
* @param {string} registryUrl - Registry URL or path
|
|
1426
|
+
*/
|
|
855
1427
|
async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
856
1428
|
try {
|
|
857
1429
|
const index = await loadIndex(registryUrl);
|
|
858
1430
|
|
|
859
1431
|
console.log('\nAvailable components:\n');
|
|
860
1432
|
index.registry.forEach(comp => {
|
|
1433
|
+
// Show dependencies if component has any
|
|
861
1434
|
const deps = comp.registryDependencies && comp.registryDependencies.length > 0
|
|
862
1435
|
? ` (depends on: ${comp.registryDependencies.join(', ')})`
|
|
863
1436
|
: '';
|
|
@@ -871,27 +1444,43 @@ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
|
871
1444
|
}
|
|
872
1445
|
|
|
873
1446
|
// ============================================================================
|
|
874
|
-
// MAIN
|
|
1447
|
+
// MAIN - CLI Entry Point
|
|
875
1448
|
// ============================================================================
|
|
876
1449
|
|
|
1450
|
+
/**
|
|
1451
|
+
* Main CLI entry point
|
|
1452
|
+
*
|
|
1453
|
+
* Parses command line arguments and routes to appropriate command handler:
|
|
1454
|
+
* - add: Install a component
|
|
1455
|
+
* - list: List available components
|
|
1456
|
+
* - css: Install/update CSS styles
|
|
1457
|
+
*
|
|
1458
|
+
* Supports options:
|
|
1459
|
+
* - --registry: Custom registry URL or path
|
|
1460
|
+
* - --overwrite / -f: Overwrite existing files
|
|
1461
|
+
*/
|
|
877
1462
|
async function main() {
|
|
878
1463
|
const args = process.argv.slice(2);
|
|
879
1464
|
const command = args[0];
|
|
880
1465
|
const componentName = args[1];
|
|
881
1466
|
|
|
1467
|
+
// Parse --registry option if provided
|
|
882
1468
|
const registryIndex = args.indexOf('--registry');
|
|
883
1469
|
const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
|
|
884
1470
|
? args[registryIndex + 1]
|
|
885
1471
|
: DEFAULT_REGISTRY_URL;
|
|
886
1472
|
|
|
1473
|
+
// Build options object
|
|
887
1474
|
const options = {
|
|
888
1475
|
overwrite: args.includes('--overwrite') || args.includes('-f'),
|
|
889
1476
|
registryUrl: registryUrl,
|
|
890
1477
|
};
|
|
891
1478
|
|
|
1479
|
+
// Show usage if no command provided
|
|
892
1480
|
if (!command) {
|
|
893
1481
|
console.log(`
|
|
894
1482
|
Usage:
|
|
1483
|
+
node scripts/cli-remote.js init [--registry <url>] Initialize project
|
|
895
1484
|
node scripts/cli-remote.js add <component-name> [--registry <url>] Install a component
|
|
896
1485
|
node scripts/cli-remote.js list [--registry <url>] List all available components
|
|
897
1486
|
node scripts/cli-remote.js css [--registry <url>] [--overwrite] Install/update CSS styles
|
|
@@ -901,6 +1490,7 @@ Options:
|
|
|
901
1490
|
--overwrite, -f Overwrite existing files
|
|
902
1491
|
|
|
903
1492
|
Examples:
|
|
1493
|
+
node scripts/cli-remote.js init
|
|
904
1494
|
node scripts/cli-remote.js add button
|
|
905
1495
|
node scripts/cli-remote.js add button --registry https://raw.githubusercontent.com/user/repo/main/registry
|
|
906
1496
|
node scripts/cli-remote.js list
|
|
@@ -910,7 +1500,12 @@ Examples:
|
|
|
910
1500
|
process.exit(0);
|
|
911
1501
|
}
|
|
912
1502
|
|
|
1503
|
+
// Route to appropriate command handler
|
|
913
1504
|
switch (command) {
|
|
1505
|
+
case 'init':
|
|
1506
|
+
await initProject();
|
|
1507
|
+
break;
|
|
1508
|
+
|
|
914
1509
|
case 'add':
|
|
915
1510
|
if (!componentName) {
|
|
916
1511
|
error('Please provide a component name');
|
|
@@ -926,17 +1521,19 @@ Examples:
|
|
|
926
1521
|
|
|
927
1522
|
case 'css':
|
|
928
1523
|
case 'styles':
|
|
1524
|
+
// Both 'css' and 'styles' commands do the same thing
|
|
929
1525
|
const config = loadComponentsConfig();
|
|
930
1526
|
await installCSSStyles(config, registryUrl, options.overwrite, false);
|
|
931
1527
|
break;
|
|
932
1528
|
|
|
933
1529
|
default:
|
|
934
1530
|
error(`Unknown command: ${command}`);
|
|
935
|
-
console.log('Available commands: add, list, css');
|
|
1531
|
+
console.log('Available commands: init, add, list, css');
|
|
936
1532
|
process.exit(1);
|
|
937
1533
|
}
|
|
938
1534
|
}
|
|
939
1535
|
|
|
1536
|
+
// Run main function and handle errors
|
|
940
1537
|
main().catch(err => {
|
|
941
1538
|
error(`Error: ${err.message}`);
|
|
942
1539
|
process.exit(1);
|