@gilav21/shadcn-angular 0.0.22 → 0.0.24
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/dist/commands/add.d.ts +13 -1
- package/dist/commands/add.js +39 -20
- package/dist/commands/add.spec.js +301 -11
- package/dist/index.js +1 -1
- package/dist/registry/index.js +16 -0
- package/package.json +1 -1
- package/src/commands/add.spec.ts +441 -12
- package/src/commands/add.ts +621 -598
- package/src/commands/init.ts +314 -314
- package/src/registry/index.ts +16 -0
package/src/commands/add.ts
CHANGED
|
@@ -1,598 +1,621 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import prompts from 'prompts';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import ora, { type Ora } from 'ora';
|
|
7
|
-
import { getConfig, type Config } from '../utils/config.js';
|
|
8
|
-
import { registry, type ComponentDefinition, type ComponentName } from '../registry/index.js';
|
|
9
|
-
import { installPackages } from '../utils/package-manager.js';
|
|
10
|
-
import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
-
|
|
15
|
-
interface AddOptions {
|
|
16
|
-
yes?: boolean;
|
|
17
|
-
overwrite?: boolean;
|
|
18
|
-
all?: boolean;
|
|
19
|
-
path?: string;
|
|
20
|
-
remote?: boolean;
|
|
21
|
-
branch: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ConflictCheckResult {
|
|
25
|
-
toInstall: ComponentName[];
|
|
26
|
-
toSkip: string[];
|
|
27
|
-
conflicting: ComponentName[];
|
|
28
|
-
peerFilesToUpdate: Set<string>;
|
|
29
|
-
contentCache: Map<string, string>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Path & URL helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
function validateBranch(branch: string): void {
|
|
37
|
-
if (!/^[\w.\-/]+$/.test(branch)) {
|
|
38
|
-
throw new Error(`Invalid branch name: ${branch}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getRegistryBaseUrl(branch: string) {
|
|
43
|
-
validateBranch(branch);
|
|
44
|
-
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/ui`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getLibRegistryBaseUrl(branch: string) {
|
|
48
|
-
validateBranch(branch);
|
|
49
|
-
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/lib`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function getLocalComponentsDir(): string | null {
|
|
53
|
-
const localPath = path.resolve(__dirname, '../../../components/ui');
|
|
54
|
-
return fs.existsSync(localPath) ? localPath : null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getLocalLibDir(): string | null {
|
|
58
|
-
const fromDist = path.resolve(__dirname, '../../../components/lib');
|
|
59
|
-
if (fs.existsSync(fromDist)) {
|
|
60
|
-
return fromDist;
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function resolveProjectPath(cwd: string, inputPath: string): string {
|
|
66
|
-
const resolved = path.resolve(cwd, inputPath);
|
|
67
|
-
const relative = path.relative(cwd, resolved);
|
|
68
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
69
|
-
throw new Error(`Path must stay inside the project directory: ${inputPath}`);
|
|
70
|
-
}
|
|
71
|
-
return resolved;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function aliasToProjectPath(aliasOrPath: string): string {
|
|
75
|
-
return aliasOrPath.startsWith('@/')
|
|
76
|
-
? path.join('src', aliasOrPath.slice(2))
|
|
77
|
-
: aliasOrPath;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function normalizeContent(str: string): string {
|
|
81
|
-
return str.replaceAll('\r\n', '\n').trim();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Remote content fetching
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
|
|
89
|
-
const localDir = getLocalComponentsDir();
|
|
90
|
-
|
|
91
|
-
if (localDir && !options.remote) {
|
|
92
|
-
const localPath = path.join(localDir, file);
|
|
93
|
-
if (await fs.pathExists(localPath)) {
|
|
94
|
-
return fs.readFile(localPath, 'utf-8');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
|
|
99
|
-
try {
|
|
100
|
-
const response = await fetch(url);
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
|
|
103
|
-
}
|
|
104
|
-
return await response.text();
|
|
105
|
-
} catch (error) {
|
|
106
|
-
if (localDir) {
|
|
107
|
-
throw new Error(`Component file not found locally or remotely: ${file}`);
|
|
108
|
-
}
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
|
|
114
|
-
const localDir = getLocalLibDir();
|
|
115
|
-
|
|
116
|
-
if (localDir && !options.remote) {
|
|
117
|
-
const localPath = path.join(localDir, file);
|
|
118
|
-
if (await fs.pathExists(localPath)) {
|
|
119
|
-
return fs.readFile(localPath, 'utf-8');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
|
|
124
|
-
const response = await fetch(url);
|
|
125
|
-
if (!response.ok) {
|
|
126
|
-
throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
|
|
127
|
-
}
|
|
128
|
-
return response.text();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function fetchAndTransform(file: string, options: AddOptions, utilsAlias: string): Promise<string> {
|
|
132
|
-
let content = await fetchComponentContent(file, options);
|
|
133
|
-
content = content.replaceAll(/(\.\.\/)+lib
|
|
134
|
-
return content;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Component selection & dependency resolution
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
async function selectComponents(components: string[], options: AddOptions): Promise<ComponentName[]> {
|
|
142
|
-
if (options.all) {
|
|
143
|
-
return Object.keys(registry);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (components.length === 0) {
|
|
147
|
-
const { selected } = await prompts({
|
|
148
|
-
type: 'multiselect',
|
|
149
|
-
name: 'selected',
|
|
150
|
-
message: 'Which components would you like to add?',
|
|
151
|
-
choices: Object.keys(registry).map(name => ({
|
|
152
|
-
title: name,
|
|
153
|
-
value: name,
|
|
154
|
-
})),
|
|
155
|
-
hint: '- Space to select, Enter to confirm',
|
|
156
|
-
});
|
|
157
|
-
return selected;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return components;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function validateComponents(names: ComponentName[]): void {
|
|
164
|
-
const invalid = names.filter(c => !registry[c]);
|
|
165
|
-
if (invalid.length > 0) {
|
|
166
|
-
console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
|
|
167
|
-
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function resolveDependencies(names: ComponentName[]): Set<ComponentName> {
|
|
173
|
-
const all = new Set<ComponentName>();
|
|
174
|
-
const walk = (name: ComponentName) => {
|
|
175
|
-
if (all.has(name)) return;
|
|
176
|
-
all.add(name);
|
|
177
|
-
registry[name].dependencies?.forEach(dep => walk(dep));
|
|
178
|
-
};
|
|
179
|
-
names.forEach(walk);
|
|
180
|
-
return all;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// Optional dependency prompt
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
interface OptionalChoice {
|
|
188
|
-
readonly name: string;
|
|
189
|
-
readonly description: string;
|
|
190
|
-
readonly requestedBy: string;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function promptOptionalDependencies(
|
|
194
|
-
resolved: Set<ComponentName>,
|
|
195
|
-
options: AddOptions,
|
|
196
|
-
): Promise<ComponentName[]> {
|
|
197
|
-
const seen = new Set<string>();
|
|
198
|
-
const choices: OptionalChoice[] = [];
|
|
199
|
-
|
|
200
|
-
for (const name of resolved) {
|
|
201
|
-
const component = registry[name];
|
|
202
|
-
if (!component.optionalDependencies) continue;
|
|
203
|
-
|
|
204
|
-
for (const opt of component.optionalDependencies) {
|
|
205
|
-
if (resolved.has(opt.name) || seen.has(opt.name)) continue;
|
|
206
|
-
seen.add(opt.name);
|
|
207
|
-
choices.push({ name: opt.name, description: opt.description, requestedBy: name });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (choices.length === 0) return [];
|
|
212
|
-
if (options.yes) return [];
|
|
213
|
-
if (options.all) return choices.map(c => c.name);
|
|
214
|
-
|
|
215
|
-
const { selected } = await prompts({
|
|
216
|
-
type: 'multiselect',
|
|
217
|
-
name: 'selected',
|
|
218
|
-
message: 'Optional companion components available:',
|
|
219
|
-
choices: choices.map(c => ({
|
|
220
|
-
title: c.name + ' ' + chalk.dim('- ' + c.description + ' (for ' + c.requestedBy + ')'),
|
|
221
|
-
value: c.name,
|
|
222
|
-
})),
|
|
223
|
-
hint: '- Space to select, Enter to confirm (or press Enter to skip)',
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return selected || [];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Conflict detection
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
async function checkFileConflict(
|
|
234
|
-
file: string,
|
|
235
|
-
targetDir: string,
|
|
236
|
-
options: AddOptions,
|
|
237
|
-
utilsAlias: string,
|
|
238
|
-
contentCache: Map<string, string>,
|
|
239
|
-
): Promise<'identical' | 'changed' | 'missing'> {
|
|
240
|
-
const targetPath = path.join(targetDir, file);
|
|
241
|
-
|
|
242
|
-
if (!await fs.pathExists(targetPath)) {
|
|
243
|
-
return 'missing';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
247
|
-
try {
|
|
248
|
-
const remoteContent = await fetchAndTransform(file, options, utilsAlias);
|
|
249
|
-
contentCache.set(file, remoteContent);
|
|
250
|
-
|
|
251
|
-
return normalizeContent(localContent) === normalizeContent(remoteContent)
|
|
252
|
-
? 'identical'
|
|
253
|
-
: 'changed';
|
|
254
|
-
} catch {
|
|
255
|
-
return 'changed';
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function checkPeerFiles(
|
|
260
|
-
component: ComponentDefinition,
|
|
261
|
-
targetDir: string,
|
|
262
|
-
options: AddOptions,
|
|
263
|
-
utilsAlias: string,
|
|
264
|
-
contentCache: Map<string, string>,
|
|
265
|
-
peerFilesToUpdate: Set<string>,
|
|
266
|
-
): Promise<
|
|
267
|
-
if (!component.peerFiles) return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
options
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
finalComponents
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora, { type Ora } from 'ora';
|
|
7
|
+
import { getConfig, type Config } from '../utils/config.js';
|
|
8
|
+
import { registry, type ComponentDefinition, type ComponentName } from '../registry/index.js';
|
|
9
|
+
import { installPackages } from '../utils/package-manager.js';
|
|
10
|
+
import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
export interface AddOptions {
|
|
16
|
+
yes?: boolean;
|
|
17
|
+
overwrite?: boolean;
|
|
18
|
+
all?: boolean;
|
|
19
|
+
path?: string;
|
|
20
|
+
remote?: boolean;
|
|
21
|
+
branch: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ConflictCheckResult {
|
|
25
|
+
toInstall: ComponentName[];
|
|
26
|
+
toSkip: string[];
|
|
27
|
+
conflicting: ComponentName[];
|
|
28
|
+
peerFilesToUpdate: Set<string>;
|
|
29
|
+
contentCache: Map<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Path & URL helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function validateBranch(branch: string): void {
|
|
37
|
+
if (!/^[\w.\-/]+$/.test(branch)) {
|
|
38
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getRegistryBaseUrl(branch: string) {
|
|
43
|
+
validateBranch(branch);
|
|
44
|
+
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/ui`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLibRegistryBaseUrl(branch: string) {
|
|
48
|
+
validateBranch(branch);
|
|
49
|
+
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/lib`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLocalComponentsDir(): string | null {
|
|
53
|
+
const localPath = path.resolve(__dirname, '../../../components/ui');
|
|
54
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getLocalLibDir(): string | null {
|
|
58
|
+
const fromDist = path.resolve(__dirname, '../../../components/lib');
|
|
59
|
+
if (fs.existsSync(fromDist)) {
|
|
60
|
+
return fromDist;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveProjectPath(cwd: string, inputPath: string): string {
|
|
66
|
+
const resolved = path.resolve(cwd, inputPath);
|
|
67
|
+
const relative = path.relative(cwd, resolved);
|
|
68
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
69
|
+
throw new Error(`Path must stay inside the project directory: ${inputPath}`);
|
|
70
|
+
}
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function aliasToProjectPath(aliasOrPath: string): string {
|
|
75
|
+
return aliasOrPath.startsWith('@/')
|
|
76
|
+
? path.join('src', aliasOrPath.slice(2))
|
|
77
|
+
: aliasOrPath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeContent(str: string): string {
|
|
81
|
+
return str.replaceAll('\r\n', '\n').trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Remote content fetching
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
|
|
89
|
+
const localDir = getLocalComponentsDir();
|
|
90
|
+
|
|
91
|
+
if (localDir && !options.remote) {
|
|
92
|
+
const localPath = path.join(localDir, file);
|
|
93
|
+
if (await fs.pathExists(localPath)) {
|
|
94
|
+
return fs.readFile(localPath, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(url);
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
|
|
103
|
+
}
|
|
104
|
+
return await response.text();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (localDir) {
|
|
107
|
+
throw new Error(`Component file not found locally or remotely: ${file}`);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
|
|
114
|
+
const localDir = getLocalLibDir();
|
|
115
|
+
|
|
116
|
+
if (localDir && !options.remote) {
|
|
117
|
+
const localPath = path.join(localDir, file);
|
|
118
|
+
if (await fs.pathExists(localPath)) {
|
|
119
|
+
return fs.readFile(localPath, 'utf-8');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
|
|
124
|
+
const response = await fetch(url);
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
return response.text();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function fetchAndTransform(file: string, options: AddOptions, utilsAlias: string): Promise<string> {
|
|
132
|
+
let content = await fetchComponentContent(file, options);
|
|
133
|
+
content = content.replaceAll(/(\.\.\/)+lib\//g, utilsAlias + '/');
|
|
134
|
+
return content;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Component selection & dependency resolution
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
async function selectComponents(components: string[], options: AddOptions): Promise<ComponentName[]> {
|
|
142
|
+
if (options.all) {
|
|
143
|
+
return Object.keys(registry);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (components.length === 0) {
|
|
147
|
+
const { selected } = await prompts({
|
|
148
|
+
type: 'multiselect',
|
|
149
|
+
name: 'selected',
|
|
150
|
+
message: 'Which components would you like to add?',
|
|
151
|
+
choices: Object.keys(registry).map(name => ({
|
|
152
|
+
title: name,
|
|
153
|
+
value: name,
|
|
154
|
+
})),
|
|
155
|
+
hint: '- Space to select, Enter to confirm',
|
|
156
|
+
});
|
|
157
|
+
return selected;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return components;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateComponents(names: ComponentName[]): void {
|
|
164
|
+
const invalid = names.filter(c => !registry[c]);
|
|
165
|
+
if (invalid.length > 0) {
|
|
166
|
+
console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
|
|
167
|
+
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resolveDependencies(names: ComponentName[]): Set<ComponentName> {
|
|
173
|
+
const all = new Set<ComponentName>();
|
|
174
|
+
const walk = (name: ComponentName) => {
|
|
175
|
+
if (all.has(name)) return;
|
|
176
|
+
all.add(name);
|
|
177
|
+
registry[name].dependencies?.forEach(dep => walk(dep));
|
|
178
|
+
};
|
|
179
|
+
names.forEach(walk);
|
|
180
|
+
return all;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Optional dependency prompt
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
interface OptionalChoice {
|
|
188
|
+
readonly name: string;
|
|
189
|
+
readonly description: string;
|
|
190
|
+
readonly requestedBy: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function promptOptionalDependencies(
|
|
194
|
+
resolved: Set<ComponentName>,
|
|
195
|
+
options: AddOptions,
|
|
196
|
+
): Promise<ComponentName[]> {
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
const choices: OptionalChoice[] = [];
|
|
199
|
+
|
|
200
|
+
for (const name of resolved) {
|
|
201
|
+
const component = registry[name];
|
|
202
|
+
if (!component.optionalDependencies) continue;
|
|
203
|
+
|
|
204
|
+
for (const opt of component.optionalDependencies) {
|
|
205
|
+
if (resolved.has(opt.name) || seen.has(opt.name)) continue;
|
|
206
|
+
seen.add(opt.name);
|
|
207
|
+
choices.push({ name: opt.name, description: opt.description, requestedBy: name });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (choices.length === 0) return [];
|
|
212
|
+
if (options.yes) return [];
|
|
213
|
+
if (options.all) return choices.map(c => c.name);
|
|
214
|
+
|
|
215
|
+
const { selected } = await prompts({
|
|
216
|
+
type: 'multiselect',
|
|
217
|
+
name: 'selected',
|
|
218
|
+
message: 'Optional companion components available:',
|
|
219
|
+
choices: choices.map(c => ({
|
|
220
|
+
title: c.name + ' ' + chalk.dim('- ' + c.description + ' (for ' + c.requestedBy + ')'),
|
|
221
|
+
value: c.name,
|
|
222
|
+
})),
|
|
223
|
+
hint: '- Space to select, Enter to confirm (or press Enter to skip)',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return selected || [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Conflict detection
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
export async function checkFileConflict(
|
|
234
|
+
file: string,
|
|
235
|
+
targetDir: string,
|
|
236
|
+
options: AddOptions,
|
|
237
|
+
utilsAlias: string,
|
|
238
|
+
contentCache: Map<string, string>,
|
|
239
|
+
): Promise<'identical' | 'changed' | 'missing'> {
|
|
240
|
+
const targetPath = path.join(targetDir, file);
|
|
241
|
+
|
|
242
|
+
if (!await fs.pathExists(targetPath)) {
|
|
243
|
+
return 'missing';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
247
|
+
try {
|
|
248
|
+
const remoteContent = await fetchAndTransform(file, options, utilsAlias);
|
|
249
|
+
contentCache.set(file, remoteContent);
|
|
250
|
+
|
|
251
|
+
return normalizeContent(localContent) === normalizeContent(remoteContent)
|
|
252
|
+
? 'identical'
|
|
253
|
+
: 'changed';
|
|
254
|
+
} catch {
|
|
255
|
+
return 'changed';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function checkPeerFiles(
|
|
260
|
+
component: ComponentDefinition,
|
|
261
|
+
targetDir: string,
|
|
262
|
+
options: AddOptions,
|
|
263
|
+
utilsAlias: string,
|
|
264
|
+
contentCache: Map<string, string>,
|
|
265
|
+
peerFilesToUpdate: Set<string>,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
if (!component.peerFiles) return;
|
|
268
|
+
|
|
269
|
+
for (const file of component.peerFiles) {
|
|
270
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
271
|
+
if (status === 'changed') {
|
|
272
|
+
peerFilesToUpdate.add(file);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function classifyComponent(
|
|
278
|
+
name: ComponentName,
|
|
279
|
+
targetDir: string,
|
|
280
|
+
options: AddOptions,
|
|
281
|
+
utilsAlias: string,
|
|
282
|
+
contentCache: Map<string, string>,
|
|
283
|
+
peerFilesToUpdate: Set<string>,
|
|
284
|
+
): Promise<'install' | 'skip' | 'conflict'> {
|
|
285
|
+
const component = registry[name];
|
|
286
|
+
let ownFilesChanged = false;
|
|
287
|
+
let isFullyPresent = true;
|
|
288
|
+
|
|
289
|
+
for (const file of component.files) {
|
|
290
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
291
|
+
if (status === 'missing') isFullyPresent = false;
|
|
292
|
+
if (status === 'changed') ownFilesChanged = true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await checkPeerFiles(
|
|
296
|
+
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (isFullyPresent && !ownFilesChanged) return 'skip';
|
|
300
|
+
if (ownFilesChanged) return 'conflict';
|
|
301
|
+
return 'install';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function detectConflicts(
|
|
305
|
+
allComponents: Set<ComponentName>,
|
|
306
|
+
targetDir: string,
|
|
307
|
+
options: AddOptions,
|
|
308
|
+
utilsAlias: string,
|
|
309
|
+
): Promise<ConflictCheckResult> {
|
|
310
|
+
const toInstall: ComponentName[] = [];
|
|
311
|
+
const toSkip: string[] = [];
|
|
312
|
+
const conflicting: ComponentName[] = [];
|
|
313
|
+
const peerFilesToUpdate = new Set<string>();
|
|
314
|
+
const contentCache = new Map<string, string>();
|
|
315
|
+
|
|
316
|
+
for (const name of allComponents) {
|
|
317
|
+
const result = await classifyComponent(
|
|
318
|
+
name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (result === 'skip') toSkip.push(name);
|
|
322
|
+
else if (result === 'conflict') conflicting.push(name);
|
|
323
|
+
else toInstall.push(name);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Overwrite prompt
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
async function promptOverwrite(
|
|
334
|
+
conflicting: ComponentName[],
|
|
335
|
+
options: AddOptions,
|
|
336
|
+
): Promise<ComponentName[]> {
|
|
337
|
+
if (conflicting.length === 0) return [];
|
|
338
|
+
|
|
339
|
+
if (options.overwrite) return conflicting;
|
|
340
|
+
if (options.yes) return [];
|
|
341
|
+
|
|
342
|
+
console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote.`));
|
|
343
|
+
const { selected } = await prompts({
|
|
344
|
+
type: 'multiselect',
|
|
345
|
+
name: 'selected',
|
|
346
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
347
|
+
choices: conflicting.map(name => ({ title: name, value: name })),
|
|
348
|
+
hint: '- Space to select, Enter to confirm',
|
|
349
|
+
});
|
|
350
|
+
return selected || [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// File writing
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
async function writeComponentFiles(
|
|
358
|
+
component: ComponentDefinition,
|
|
359
|
+
targetDir: string,
|
|
360
|
+
options: AddOptions,
|
|
361
|
+
utilsAlias: string,
|
|
362
|
+
contentCache: Map<string, string>,
|
|
363
|
+
spinner: Ora,
|
|
364
|
+
): Promise<boolean> {
|
|
365
|
+
let success = true;
|
|
366
|
+
|
|
367
|
+
for (const file of component.files) {
|
|
368
|
+
const targetPath = path.join(targetDir, file);
|
|
369
|
+
try {
|
|
370
|
+
const content = contentCache.get(file)
|
|
371
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
372
|
+
|
|
373
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
374
|
+
await fs.writeFile(targetPath, content);
|
|
375
|
+
} catch (err: unknown) {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
spinner.warn(`Could not add ${file}: ${message}`);
|
|
378
|
+
success = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return success;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function writePeerFiles(
|
|
386
|
+
component: ComponentDefinition,
|
|
387
|
+
targetDir: string,
|
|
388
|
+
options: AddOptions,
|
|
389
|
+
utilsAlias: string,
|
|
390
|
+
contentCache: Map<string, string>,
|
|
391
|
+
peerFilesToUpdate: Set<string>,
|
|
392
|
+
spinner: Ora,
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
if (!component.peerFiles) return;
|
|
395
|
+
|
|
396
|
+
for (const file of component.peerFiles) {
|
|
397
|
+
if (!peerFilesToUpdate.has(file)) continue;
|
|
398
|
+
|
|
399
|
+
const targetPath = path.join(targetDir, file);
|
|
400
|
+
try {
|
|
401
|
+
const content = contentCache.get(file)
|
|
402
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
403
|
+
|
|
404
|
+
await fs.writeFile(targetPath, content);
|
|
405
|
+
spinner.text = `Updated peer file ${file}`;
|
|
406
|
+
} catch (err: unknown) {
|
|
407
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
408
|
+
spinner.warn(`Could not update peer file ${file}: ${message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function installLibFiles(
|
|
414
|
+
allComponents: Set<ComponentName>,
|
|
415
|
+
cwd: string,
|
|
416
|
+
libDir: string,
|
|
417
|
+
options: AddOptions,
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
const required = new Set<string>();
|
|
420
|
+
for (const name of allComponents) {
|
|
421
|
+
registry[name].libFiles?.forEach(f => required.add(f));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (required.size === 0) return;
|
|
425
|
+
|
|
426
|
+
await fs.ensureDir(libDir);
|
|
427
|
+
for (const libFile of required) {
|
|
428
|
+
const targetPath = path.join(libDir, libFile);
|
|
429
|
+
if (!await fs.pathExists(targetPath) || options.overwrite) {
|
|
430
|
+
try {
|
|
431
|
+
const content = await fetchLibContent(libFile, options);
|
|
432
|
+
await fs.writeFile(targetPath, content);
|
|
433
|
+
} catch (err: unknown) {
|
|
434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
+
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function installNpmDependencies(
|
|
442
|
+
finalComponents: ComponentName[],
|
|
443
|
+
cwd: string,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
const deps = new Set<string>();
|
|
446
|
+
for (const name of finalComponents) {
|
|
447
|
+
registry[name].npmDependencies?.forEach(dep => deps.add(dep));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (deps.size === 0) return;
|
|
451
|
+
|
|
452
|
+
const spinner = ora('Installing dependencies...').start();
|
|
453
|
+
try {
|
|
454
|
+
await installPackages(Array.from(deps), { cwd });
|
|
455
|
+
spinner.succeed('Dependencies installed.');
|
|
456
|
+
} catch (e) {
|
|
457
|
+
spinner.fail('Failed to install dependencies.');
|
|
458
|
+
console.error(e);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function ensureShortcutService(
|
|
463
|
+
targetDir: string,
|
|
464
|
+
cwd: string,
|
|
465
|
+
config: Config,
|
|
466
|
+
options: AddOptions,
|
|
467
|
+
): Promise<void> {
|
|
468
|
+
const entries = collectInstalledShortcutEntries(targetDir);
|
|
469
|
+
|
|
470
|
+
if (entries.length > 0) {
|
|
471
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
472
|
+
const servicePath = path.join(libDir, 'shortcut-binding.service.ts');
|
|
473
|
+
if (!await fs.pathExists(servicePath)) {
|
|
474
|
+
const content = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
475
|
+
await fs.ensureDir(libDir);
|
|
476
|
+
await fs.writeFile(servicePath, content);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await writeShortcutRegistryIndex(cwd, config, entries);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEntry[] {
|
|
484
|
+
const entries: ShortcutRegistryEntry[] = [];
|
|
485
|
+
for (const definition of Object.values(registry)) {
|
|
486
|
+
if (!definition.shortcutDefinitions?.length) continue;
|
|
487
|
+
for (const sd of definition.shortcutDefinitions) {
|
|
488
|
+
if (fs.existsSync(path.join(targetDir, sd.sourceFile))) {
|
|
489
|
+
entries.push(sd);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return entries;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Main entry point
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
export async function add(components: string[], options: AddOptions) {
|
|
501
|
+
const cwd = process.cwd();
|
|
502
|
+
|
|
503
|
+
const config = await getConfig(cwd);
|
|
504
|
+
if (!config) {
|
|
505
|
+
console.log(chalk.red('Error: components.json not found.'));
|
|
506
|
+
console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const componentsToAdd = await selectComponents(components, options);
|
|
511
|
+
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
512
|
+
console.log(chalk.dim('No components selected.'));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
validateComponents(componentsToAdd);
|
|
517
|
+
|
|
518
|
+
const resolvedComponents = resolveDependencies(componentsToAdd);
|
|
519
|
+
const optionalChoices = await promptOptionalDependencies(resolvedComponents, options);
|
|
520
|
+
const allComponents = optionalChoices.length > 0
|
|
521
|
+
? resolveDependencies([...resolvedComponents, ...optionalChoices])
|
|
522
|
+
: resolvedComponents;
|
|
523
|
+
const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
|
|
524
|
+
const targetDir = resolveProjectPath(cwd, uiBasePath);
|
|
525
|
+
const utilsAlias = config.aliases.utils;
|
|
526
|
+
|
|
527
|
+
// Detect conflicts
|
|
528
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
529
|
+
const { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache } =
|
|
530
|
+
await detectConflicts(allComponents, targetDir, options, utilsAlias);
|
|
531
|
+
checkSpinner.stop();
|
|
532
|
+
|
|
533
|
+
// Prompt user for overwrite decisions
|
|
534
|
+
const toOverwrite = await promptOverwrite(conflicting, options);
|
|
535
|
+
const finalComponents = [...toInstall, ...toOverwrite];
|
|
536
|
+
|
|
537
|
+
// Remove peer files that belong only to declined components
|
|
538
|
+
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
539
|
+
for (const name of declined) {
|
|
540
|
+
const component = registry[name];
|
|
541
|
+
if (!component.peerFiles) continue;
|
|
542
|
+
for (const file of component.peerFiles) {
|
|
543
|
+
const stillNeeded = finalComponents.some(fc =>
|
|
544
|
+
registry[fc].peerFiles?.includes(file),
|
|
545
|
+
);
|
|
546
|
+
if (!stillNeeded) {
|
|
547
|
+
peerFilesToUpdate.delete(file);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (finalComponents.length === 0) {
|
|
553
|
+
if (toSkip.length > 0 || declined.length > 0) {
|
|
554
|
+
if (toSkip.length > 0) {
|
|
555
|
+
console.log(chalk.green(`\nAll components are up to date! (${toSkip.length} skipped)`));
|
|
556
|
+
}
|
|
557
|
+
if (declined.length > 0) {
|
|
558
|
+
console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
|
|
559
|
+
declined.forEach(name => console.log(chalk.dim(' - ') + chalk.yellow(name)));
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Install component files
|
|
568
|
+
const spinner = ora('Installing components...').start();
|
|
569
|
+
let successCount = 0;
|
|
570
|
+
const finalComponentSet = new Set(finalComponents);
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
await fs.ensureDir(targetDir);
|
|
574
|
+
|
|
575
|
+
for (const name of finalComponents) {
|
|
576
|
+
const component = registry[name];
|
|
577
|
+
|
|
578
|
+
const ok = await writeComponentFiles(
|
|
579
|
+
component, targetDir, options, utilsAlias, contentCache, spinner,
|
|
580
|
+
);
|
|
581
|
+
await writePeerFiles(
|
|
582
|
+
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
if (ok) {
|
|
586
|
+
successCount++;
|
|
587
|
+
spinner.text = `Added ${name}`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (successCount > 0) {
|
|
592
|
+
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
593
|
+
console.log('\n' + chalk.dim('Components added:'));
|
|
594
|
+
finalComponents.forEach(name => console.log(chalk.dim(' - ') + chalk.cyan(name)));
|
|
595
|
+
} else {
|
|
596
|
+
spinner.info('No new components installed.');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Post-install: lib files, npm deps, shortcuts
|
|
600
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
|
|
601
|
+
await installLibFiles(finalComponentSet, cwd, libDir, options);
|
|
602
|
+
await installNpmDependencies(finalComponents, cwd);
|
|
603
|
+
await ensureShortcutService(targetDir, cwd, config, options);
|
|
604
|
+
|
|
605
|
+
if (toSkip.length > 0) {
|
|
606
|
+
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
607
|
+
toSkip.forEach(name => console.log(chalk.dim(' - ') + chalk.gray(name)));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (declined.length > 0) {
|
|
611
|
+
console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
|
|
612
|
+
declined.forEach(name => console.log(chalk.dim(' - ') + chalk.yellow(name)));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
console.log('');
|
|
616
|
+
} catch (error) {
|
|
617
|
+
spinner.fail('Failed to add components');
|
|
618
|
+
console.error(error);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
}
|