@gilav21/shadcn-angular 0.0.25 → 0.0.26
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 +2 -0
- package/dist/commands/add.js +169 -114
- package/dist/commands/add.spec.js +17 -23
- package/dist/commands/diff.d.ts +8 -0
- package/dist/commands/diff.js +99 -0
- package/dist/commands/help.js +15 -6
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +171 -185
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +50 -0
- package/dist/index.js +21 -1
- package/dist/registry/index.d.ts +122 -12
- package/dist/registry/index.js +56 -168
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +22 -2
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/shortcut-registry.js +1 -13
- package/package.json +1 -1
- package/scripts/sync-registry.ts +347 -0
- package/src/commands/add.spec.ts +22 -32
- package/src/commands/add.ts +211 -137
- package/src/commands/diff.ts +133 -0
- package/src/commands/help.ts +15 -6
- package/src/commands/init.ts +329 -314
- package/src/commands/list.ts +66 -0
- package/src/index.ts +24 -1
- package/src/registry/index.ts +71 -180
- package/src/utils/config.ts +22 -3
- package/src/utils/paths.ts +52 -0
- package/src/utils/shortcut-registry.ts +1 -15
- package/vitest.config.ts +7 -0
package/src/commands/add.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import prompts from 'prompts';
|
|
5
4
|
import chalk from 'chalk';
|
|
6
5
|
import ora, { type Ora } from 'ora';
|
|
7
6
|
import { getConfig, type Config } from '../utils/config.js';
|
|
8
|
-
import { registry, type ComponentDefinition, type ComponentName } from '../registry/index.js';
|
|
7
|
+
import { registry, getComponentNames, type ComponentDefinition, type ComponentName } from '../registry/index.js';
|
|
9
8
|
import { installPackages } from '../utils/package-manager.js';
|
|
10
9
|
import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
getRegistryBaseUrl,
|
|
12
|
+
getLibRegistryBaseUrl,
|
|
13
|
+
getLocalComponentsDir,
|
|
14
|
+
getLocalLibDir,
|
|
15
|
+
resolveProjectPath,
|
|
16
|
+
aliasToProjectPath,
|
|
17
|
+
} from '../utils/paths.js';
|
|
18
|
+
|
|
19
|
+
const onCancel = () => {
|
|
20
|
+
console.log(chalk.dim('\nCancelled.'));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
};
|
|
14
23
|
|
|
15
24
|
export interface AddOptions {
|
|
16
25
|
yes?: boolean;
|
|
@@ -18,7 +27,9 @@ export interface AddOptions {
|
|
|
18
27
|
all?: boolean;
|
|
19
28
|
path?: string;
|
|
20
29
|
remote?: boolean;
|
|
30
|
+
dryRun?: boolean;
|
|
21
31
|
branch: string;
|
|
32
|
+
registry?: string;
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
interface ConflictCheckResult {
|
|
@@ -29,54 +40,6 @@ interface ConflictCheckResult {
|
|
|
29
40
|
contentCache: Map<string, string>;
|
|
30
41
|
}
|
|
31
42
|
|
|
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
43
|
export function normalizeContent(str: string): string {
|
|
81
44
|
return str.replaceAll('\r\n', '\n').trim();
|
|
82
45
|
}
|
|
@@ -95,7 +58,7 @@ async function fetchComponentContent(file: string, options: AddOptions): Promise
|
|
|
95
58
|
}
|
|
96
59
|
}
|
|
97
60
|
|
|
98
|
-
const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
|
|
61
|
+
const url = `${getRegistryBaseUrl(options.branch, options.registry)}/${file}`;
|
|
99
62
|
try {
|
|
100
63
|
const response = await fetch(url);
|
|
101
64
|
if (!response.ok) {
|
|
@@ -120,7 +83,7 @@ async function fetchLibContent(file: string, options: AddOptions): Promise<strin
|
|
|
120
83
|
}
|
|
121
84
|
}
|
|
122
85
|
|
|
123
|
-
const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
|
|
86
|
+
const url = `${getLibRegistryBaseUrl(options.branch, options.registry)}/${file}`;
|
|
124
87
|
const response = await fetch(url);
|
|
125
88
|
if (!response.ok) {
|
|
126
89
|
throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
|
|
@@ -140,7 +103,7 @@ export async function fetchAndTransform(file: string, options: AddOptions, utils
|
|
|
140
103
|
|
|
141
104
|
async function selectComponents(components: string[], options: AddOptions): Promise<ComponentName[]> {
|
|
142
105
|
if (options.all) {
|
|
143
|
-
return
|
|
106
|
+
return getComponentNames();
|
|
144
107
|
}
|
|
145
108
|
|
|
146
109
|
if (components.length === 0) {
|
|
@@ -148,23 +111,23 @@ async function selectComponents(components: string[], options: AddOptions): Prom
|
|
|
148
111
|
type: 'multiselect',
|
|
149
112
|
name: 'selected',
|
|
150
113
|
message: 'Which components would you like to add?',
|
|
151
|
-
choices:
|
|
114
|
+
choices: getComponentNames().map(name => ({
|
|
152
115
|
title: name,
|
|
153
116
|
value: name,
|
|
154
117
|
})),
|
|
155
118
|
hint: '- Space to select, Enter to confirm',
|
|
156
|
-
});
|
|
119
|
+
}, { onCancel });
|
|
157
120
|
return selected;
|
|
158
121
|
}
|
|
159
122
|
|
|
160
|
-
return components;
|
|
123
|
+
return components as ComponentName[];
|
|
161
124
|
}
|
|
162
125
|
|
|
163
126
|
function validateComponents(names: ComponentName[]): void {
|
|
164
|
-
const invalid = names.filter(c => !registry
|
|
127
|
+
const invalid = names.filter(c => !(c in registry));
|
|
165
128
|
if (invalid.length > 0) {
|
|
166
129
|
console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
|
|
167
|
-
console.log(chalk.dim('Available components: ' +
|
|
130
|
+
console.log(chalk.dim('Available components: ' + getComponentNames().join(', ')));
|
|
168
131
|
process.exit(1);
|
|
169
132
|
}
|
|
170
133
|
}
|
|
@@ -174,9 +137,11 @@ export function resolveDependencies(names: ComponentName[]): Set<ComponentName>
|
|
|
174
137
|
const walk = (name: ComponentName) => {
|
|
175
138
|
if (all.has(name)) return;
|
|
176
139
|
all.add(name);
|
|
177
|
-
registry[name].dependencies
|
|
140
|
+
for (const dep of registry[name].dependencies ?? []) {
|
|
141
|
+
walk(dep as ComponentName);
|
|
142
|
+
}
|
|
178
143
|
};
|
|
179
|
-
names
|
|
144
|
+
for (const name of names) walk(name);
|
|
180
145
|
return all;
|
|
181
146
|
}
|
|
182
147
|
|
|
@@ -202,7 +167,7 @@ export async function promptOptionalDependencies(
|
|
|
202
167
|
if (!component.optionalDependencies) continue;
|
|
203
168
|
|
|
204
169
|
for (const opt of component.optionalDependencies) {
|
|
205
|
-
if (resolved.has(opt.name) || seen.has(opt.name)) continue;
|
|
170
|
+
if (resolved.has(opt.name as ComponentName) || seen.has(opt.name)) continue;
|
|
206
171
|
seen.add(opt.name);
|
|
207
172
|
choices.push({ name: opt.name, description: opt.description, requestedBy: name });
|
|
208
173
|
}
|
|
@@ -210,7 +175,7 @@ export async function promptOptionalDependencies(
|
|
|
210
175
|
|
|
211
176
|
if (choices.length === 0) return [];
|
|
212
177
|
if (options.yes) return [];
|
|
213
|
-
if (options.all) return choices.map(c => c.name);
|
|
178
|
+
if (options.all) return choices.map(c => c.name as ComponentName);
|
|
214
179
|
|
|
215
180
|
const { selected } = await prompts({
|
|
216
181
|
type: 'multiselect',
|
|
@@ -221,7 +186,7 @@ export async function promptOptionalDependencies(
|
|
|
221
186
|
value: c.name,
|
|
222
187
|
})),
|
|
223
188
|
hint: '- Space to select, Enter to confirm (or press Enter to skip)',
|
|
224
|
-
});
|
|
189
|
+
}, { onCancel });
|
|
225
190
|
|
|
226
191
|
return selected || [];
|
|
227
192
|
}
|
|
@@ -296,11 +261,32 @@ export async function classifyComponent(
|
|
|
296
261
|
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
297
262
|
);
|
|
298
263
|
|
|
264
|
+
if (options.overwrite) return isFullyPresent ? 'conflict' : 'install';
|
|
299
265
|
if (isFullyPresent && !ownFilesChanged) return 'skip';
|
|
300
266
|
if (ownFilesChanged) return 'conflict';
|
|
301
267
|
return 'install';
|
|
302
268
|
}
|
|
303
269
|
|
|
270
|
+
class ConcurrencyLimiter {
|
|
271
|
+
private active = 0;
|
|
272
|
+
private readonly queue: Array<() => void> = [];
|
|
273
|
+
|
|
274
|
+
constructor(private readonly concurrency: number) {}
|
|
275
|
+
|
|
276
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
277
|
+
if (this.active >= this.concurrency) {
|
|
278
|
+
await new Promise<void>(resolve => this.queue.push(resolve));
|
|
279
|
+
}
|
|
280
|
+
this.active++;
|
|
281
|
+
try {
|
|
282
|
+
return await fn();
|
|
283
|
+
} finally {
|
|
284
|
+
this.active--;
|
|
285
|
+
if (this.queue.length > 0) this.queue.shift()!();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
304
290
|
export async function detectConflicts(
|
|
305
291
|
allComponents: Set<ComponentName>,
|
|
306
292
|
targetDir: string,
|
|
@@ -313,11 +299,20 @@ export async function detectConflicts(
|
|
|
313
299
|
const peerFilesToUpdate = new Set<string>();
|
|
314
300
|
const contentCache = new Map<string, string>();
|
|
315
301
|
|
|
316
|
-
|
|
317
|
-
const result = await classifyComponent(
|
|
318
|
-
name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
319
|
-
);
|
|
302
|
+
const limiter = new ConcurrencyLimiter(8);
|
|
320
303
|
|
|
304
|
+
const results = await Promise.all(
|
|
305
|
+
[...allComponents].map(name =>
|
|
306
|
+
limiter.run(async () => ({
|
|
307
|
+
name,
|
|
308
|
+
result: await classifyComponent(
|
|
309
|
+
name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
310
|
+
),
|
|
311
|
+
})),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
for (const { name, result } of results) {
|
|
321
316
|
if (result === 'skip') toSkip.push(name);
|
|
322
317
|
else if (result === 'conflict') conflicting.push(name);
|
|
323
318
|
else toInstall.push(name);
|
|
@@ -330,23 +325,54 @@ export async function detectConflicts(
|
|
|
330
325
|
// Overwrite prompt
|
|
331
326
|
// ---------------------------------------------------------------------------
|
|
332
327
|
|
|
328
|
+
function showConflictDiffs(
|
|
329
|
+
conflicting: ComponentName[],
|
|
330
|
+
targetDir: string,
|
|
331
|
+
contentCache: Map<string, string>,
|
|
332
|
+
): void {
|
|
333
|
+
for (const name of conflicting) {
|
|
334
|
+
const component = registry[name];
|
|
335
|
+
const changedFiles: string[] = [];
|
|
336
|
+
|
|
337
|
+
for (const file of component.files) {
|
|
338
|
+
const remote = contentCache.get(file);
|
|
339
|
+
if (!remote) continue;
|
|
340
|
+
const localPath = path.join(targetDir, file);
|
|
341
|
+
if (!fs.existsSync(localPath)) continue;
|
|
342
|
+
|
|
343
|
+
const local = normalizeContent(fs.readFileSync(localPath, 'utf-8'));
|
|
344
|
+
if (local !== normalizeContent(remote)) {
|
|
345
|
+
changedFiles.push(file);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (changedFiles.length > 0) {
|
|
350
|
+
console.log(chalk.dim(` ${name}: `) + chalk.yellow(changedFiles.join(', ')));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
333
355
|
async function promptOverwrite(
|
|
334
356
|
conflicting: ComponentName[],
|
|
335
357
|
options: AddOptions,
|
|
358
|
+
targetDir: string,
|
|
359
|
+
contentCache: Map<string, string>,
|
|
336
360
|
): Promise<ComponentName[]> {
|
|
337
361
|
if (conflicting.length === 0) return [];
|
|
338
362
|
|
|
339
|
-
if (options.overwrite) return conflicting;
|
|
340
|
-
|
|
363
|
+
if (options.overwrite || options.yes) return conflicting;
|
|
364
|
+
|
|
365
|
+
console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote:`));
|
|
366
|
+
showConflictDiffs(conflicting, targetDir, contentCache);
|
|
367
|
+
console.log(chalk.dim('\n Use `npx shadcn-angular diff <component>` for full diffs.\n'));
|
|
341
368
|
|
|
342
|
-
console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote.`));
|
|
343
369
|
const { selected } = await prompts({
|
|
344
370
|
type: 'multiselect',
|
|
345
371
|
name: 'selected',
|
|
346
372
|
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
347
373
|
choices: conflicting.map(name => ({ title: name, value: name })),
|
|
348
374
|
hint: '- Space to select, Enter to confirm',
|
|
349
|
-
});
|
|
375
|
+
}, { onCancel });
|
|
350
376
|
return selected || [];
|
|
351
377
|
}
|
|
352
378
|
|
|
@@ -410,6 +436,25 @@ async function writePeerFiles(
|
|
|
410
436
|
}
|
|
411
437
|
}
|
|
412
438
|
|
|
439
|
+
async function installSingleLibFile(
|
|
440
|
+
libFile: string,
|
|
441
|
+
libDir: string,
|
|
442
|
+
options: AddOptions,
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const targetPath = path.join(libDir, libFile);
|
|
445
|
+
const content = await fetchLibContent(libFile, options);
|
|
446
|
+
|
|
447
|
+
if (!await fs.pathExists(targetPath) || options.overwrite) {
|
|
448
|
+
await fs.writeFile(targetPath, content);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const local = normalizeContent(await fs.readFile(targetPath, 'utf-8'));
|
|
453
|
+
if (local !== normalizeContent(content)) {
|
|
454
|
+
console.log(chalk.yellow(` Lib file ${libFile} differs from remote (use --overwrite to update)`));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
413
458
|
async function installLibFiles(
|
|
414
459
|
allComponents: Set<ComponentName>,
|
|
415
460
|
cwd: string,
|
|
@@ -418,22 +463,18 @@ async function installLibFiles(
|
|
|
418
463
|
): Promise<void> {
|
|
419
464
|
const required = new Set<string>();
|
|
420
465
|
for (const name of allComponents) {
|
|
421
|
-
registry[name].libFiles
|
|
466
|
+
for (const f of registry[name].libFiles ?? []) required.add(f);
|
|
422
467
|
}
|
|
423
468
|
|
|
424
469
|
if (required.size === 0) return;
|
|
425
470
|
|
|
426
471
|
await fs.ensureDir(libDir);
|
|
427
472
|
for (const libFile of required) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
}
|
|
473
|
+
try {
|
|
474
|
+
await installSingleLibFile(libFile, libDir, options);
|
|
475
|
+
} catch (err: unknown) {
|
|
476
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
477
|
+
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
|
|
437
478
|
}
|
|
438
479
|
}
|
|
439
480
|
}
|
|
@@ -444,7 +485,7 @@ async function installNpmDependencies(
|
|
|
444
485
|
): Promise<void> {
|
|
445
486
|
const deps = new Set<string>();
|
|
446
487
|
for (const name of finalComponents) {
|
|
447
|
-
registry[name].npmDependencies
|
|
488
|
+
for (const dep of registry[name].npmDependencies ?? []) deps.add(dep);
|
|
448
489
|
}
|
|
449
490
|
|
|
450
491
|
if (deps.size === 0) return;
|
|
@@ -453,9 +494,13 @@ async function installNpmDependencies(
|
|
|
453
494
|
try {
|
|
454
495
|
await installPackages(Array.from(deps), { cwd });
|
|
455
496
|
spinner.succeed('Dependencies installed.');
|
|
456
|
-
} catch (e) {
|
|
497
|
+
} catch (e: unknown) {
|
|
457
498
|
spinner.fail('Failed to install dependencies.');
|
|
458
|
-
|
|
499
|
+
if (e && typeof e === 'object' && 'stderr' in e && typeof e.stderr === 'string') {
|
|
500
|
+
console.error(chalk.red(e.stderr));
|
|
501
|
+
} else {
|
|
502
|
+
console.error(e);
|
|
503
|
+
}
|
|
459
504
|
}
|
|
460
505
|
}
|
|
461
506
|
|
|
@@ -493,6 +538,67 @@ function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEnt
|
|
|
493
538
|
return entries;
|
|
494
539
|
}
|
|
495
540
|
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// Peer file & summary helpers
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
function pruneDeclinedPeerFiles(
|
|
546
|
+
declined: ComponentName[],
|
|
547
|
+
finalComponents: ComponentName[],
|
|
548
|
+
peerFilesToUpdate: Set<string>,
|
|
549
|
+
): void {
|
|
550
|
+
for (const name of declined) {
|
|
551
|
+
const component = registry[name];
|
|
552
|
+
if (!component.peerFiles) continue;
|
|
553
|
+
for (const file of component.peerFiles) {
|
|
554
|
+
const stillNeeded = finalComponents.some(fc =>
|
|
555
|
+
registry[fc].peerFiles?.includes(file),
|
|
556
|
+
);
|
|
557
|
+
if (!stillNeeded) {
|
|
558
|
+
peerFilesToUpdate.delete(file);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function printNothingToInstall(toSkip: string[], declined: ComponentName[]): void {
|
|
565
|
+
if (toSkip.length > 0 || declined.length > 0) {
|
|
566
|
+
printSkipSummary(toSkip, declined);
|
|
567
|
+
} else {
|
|
568
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function printDryRunSummary(
|
|
573
|
+
toInstall: ComponentName[],
|
|
574
|
+
toOverwrite: ComponentName[],
|
|
575
|
+
toSkip: string[],
|
|
576
|
+
declined: ComponentName[],
|
|
577
|
+
): void {
|
|
578
|
+
console.log(chalk.bold('\n[Dry Run] No changes will be made.\n'));
|
|
579
|
+
if (toInstall.length > 0) {
|
|
580
|
+
console.log(chalk.green(` Would install ${toInstall.length} component(s):`));
|
|
581
|
+
for (const name of toInstall) console.log(chalk.dim(' + ') + chalk.cyan(name));
|
|
582
|
+
}
|
|
583
|
+
if (toOverwrite.length > 0) {
|
|
584
|
+
console.log(chalk.yellow(` Would overwrite ${toOverwrite.length} component(s):`));
|
|
585
|
+
for (const name of toOverwrite) console.log(chalk.dim(' ~ ') + chalk.yellow(name));
|
|
586
|
+
}
|
|
587
|
+
printSkipSummary(toSkip, declined);
|
|
588
|
+
console.log('');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function printSkipSummary(toSkip: string[], declined: ComponentName[]): void {
|
|
592
|
+
if (toSkip.length > 0) {
|
|
593
|
+
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
594
|
+
for (const name of toSkip) console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
595
|
+
}
|
|
596
|
+
if (declined.length > 0) {
|
|
597
|
+
console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
|
|
598
|
+
for (const name of declined) console.log(chalk.dim(' - ') + chalk.yellow(name));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
496
602
|
// ---------------------------------------------------------------------------
|
|
497
603
|
// Main entry point
|
|
498
604
|
// ---------------------------------------------------------------------------
|
|
@@ -507,6 +613,11 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
507
613
|
process.exit(1);
|
|
508
614
|
}
|
|
509
615
|
|
|
616
|
+
// CLI flag takes priority over components.json
|
|
617
|
+
if (!options.registry && config.registry) {
|
|
618
|
+
options.registry = config.registry;
|
|
619
|
+
}
|
|
620
|
+
|
|
510
621
|
const componentsToAdd = await selectComponents(components, options);
|
|
511
622
|
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
512
623
|
console.log(chalk.dim('No components selected.'));
|
|
@@ -524,64 +635,37 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
524
635
|
const targetDir = resolveProjectPath(cwd, uiBasePath);
|
|
525
636
|
const utilsAlias = config.aliases.utils;
|
|
526
637
|
|
|
527
|
-
// Detect conflicts
|
|
528
638
|
const checkSpinner = ora('Checking for conflicts...').start();
|
|
529
639
|
const { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache } =
|
|
530
640
|
await detectConflicts(allComponents, targetDir, options, utilsAlias);
|
|
531
641
|
checkSpinner.stop();
|
|
532
642
|
|
|
533
|
-
|
|
534
|
-
const toOverwrite = await promptOverwrite(conflicting, options);
|
|
643
|
+
const toOverwrite = await promptOverwrite(conflicting, options, targetDir, contentCache);
|
|
535
644
|
const finalComponents = [...toInstall, ...toOverwrite];
|
|
536
|
-
|
|
537
|
-
// Remove peer files that belong only to declined components
|
|
538
645
|
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
);
|
|
546
|
-
if (!stillNeeded) {
|
|
547
|
-
peerFilesToUpdate.delete(file);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
646
|
+
|
|
647
|
+
pruneDeclinedPeerFiles(declined, finalComponents, peerFilesToUpdate);
|
|
648
|
+
|
|
649
|
+
if (options.dryRun) {
|
|
650
|
+
printDryRunSummary(toInstall, toOverwrite, toSkip, declined);
|
|
651
|
+
return;
|
|
550
652
|
}
|
|
551
653
|
|
|
552
654
|
if (finalComponents.length === 0) {
|
|
553
|
-
|
|
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
|
-
}
|
|
655
|
+
printNothingToInstall(toSkip, declined);
|
|
564
656
|
return;
|
|
565
657
|
}
|
|
566
658
|
|
|
567
|
-
// Install component files
|
|
568
659
|
const spinner = ora('Installing components...').start();
|
|
569
660
|
let successCount = 0;
|
|
570
|
-
const finalComponentSet = new Set(finalComponents);
|
|
571
661
|
|
|
572
662
|
try {
|
|
573
663
|
await fs.ensureDir(targetDir);
|
|
574
664
|
|
|
575
665
|
for (const name of finalComponents) {
|
|
576
666
|
const component = registry[name];
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
component, targetDir, options, utilsAlias, contentCache, spinner,
|
|
580
|
-
);
|
|
581
|
-
await writePeerFiles(
|
|
582
|
-
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner,
|
|
583
|
-
);
|
|
584
|
-
|
|
667
|
+
const ok = await writeComponentFiles(component, targetDir, options, utilsAlias, contentCache, spinner);
|
|
668
|
+
await writePeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner);
|
|
585
669
|
if (ok) {
|
|
586
670
|
successCount++;
|
|
587
671
|
spinner.text = `Added ${name}`;
|
|
@@ -591,27 +675,17 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
591
675
|
if (successCount > 0) {
|
|
592
676
|
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
593
677
|
console.log('\n' + chalk.dim('Components added:'));
|
|
594
|
-
|
|
678
|
+
for (const name of finalComponents) console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
595
679
|
} else {
|
|
596
680
|
spinner.info('No new components installed.');
|
|
597
681
|
}
|
|
598
682
|
|
|
599
|
-
// Post-install: lib files, npm deps, shortcuts
|
|
600
683
|
const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
|
|
601
|
-
await installLibFiles(
|
|
684
|
+
await installLibFiles(new Set(finalComponents), cwd, libDir, options);
|
|
602
685
|
await installNpmDependencies(finalComponents, cwd);
|
|
603
686
|
await ensureShortcutService(targetDir, cwd, config, options);
|
|
604
687
|
|
|
605
|
-
|
|
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
|
-
|
|
688
|
+
printSkipSummary(toSkip, declined);
|
|
615
689
|
console.log('');
|
|
616
690
|
} catch (error) {
|
|
617
691
|
spinner.fail('Failed to add components');
|