@gilav21/shadcn-angular 0.0.10 → 0.0.12
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.js +118 -25
- package/dist/commands/init.js +29 -0
- package/dist/index.js +1 -1
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +46 -2
- package/dist/utils/package-manager.d.ts +6 -0
- package/dist/utils/package-manager.js +49 -0
- package/package.json +1 -1
- package/src/commands/add.ts +128 -25
- package/src/commands/init.ts +38 -0
- package/src/index.ts +1 -1
- package/src/registry/index.ts +48 -2
- package/src/utils/package-manager.ts +50 -0
package/dist/commands/add.js
CHANGED
|
@@ -6,6 +6,7 @@ import chalk from 'chalk';
|
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import { getConfig } from '../utils/config.js';
|
|
8
8
|
import { registry } from '../registry/index.js';
|
|
9
|
+
import { installPackages } from '../utils/package-manager.js';
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = path.dirname(__filename);
|
|
11
12
|
// Base URL for the component registry (GitHub raw content)
|
|
@@ -105,67 +106,159 @@ export async function add(components, options) {
|
|
|
105
106
|
const targetDir = options.path
|
|
106
107
|
? path.join(cwd, options.path)
|
|
107
108
|
: path.join(cwd, 'src/components/ui');
|
|
108
|
-
// Check for existing files
|
|
109
|
-
const
|
|
109
|
+
// Check for existing files and diff
|
|
110
|
+
const componentsToInstall = [];
|
|
111
|
+
const componentsToSkip = [];
|
|
112
|
+
const conflictingComponents = [];
|
|
113
|
+
const contentCache = new Map();
|
|
114
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
110
115
|
for (const name of allComponents) {
|
|
111
116
|
const component = registry[name];
|
|
117
|
+
let hasChanges = false;
|
|
118
|
+
let isFullyPresent = true;
|
|
112
119
|
for (const file of component.files) {
|
|
113
120
|
const targetPath = path.join(targetDir, file);
|
|
114
121
|
if (await fs.pathExists(targetPath)) {
|
|
115
|
-
|
|
122
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
123
|
+
try {
|
|
124
|
+
let remoteContent = await fetchComponentContent(file, options);
|
|
125
|
+
// Transform imports for comparison
|
|
126
|
+
const utilsAlias = config.aliases.utils;
|
|
127
|
+
remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
|
|
128
|
+
const normalize = (str) => str.replace(/\s+/g, '').trim();
|
|
129
|
+
if (normalize(localContent) !== normalize(remoteContent)) {
|
|
130
|
+
hasChanges = true;
|
|
131
|
+
}
|
|
132
|
+
contentCache.set(file, remoteContent); // Cache for installation
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
// unexpected error fetching remote
|
|
136
|
+
console.warn(`Could not fetch remote content for comparison: ${file}`);
|
|
137
|
+
hasChanges = true; // Assume changed/unknown
|
|
138
|
+
}
|
|
116
139
|
}
|
|
140
|
+
else {
|
|
141
|
+
isFullyPresent = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (isFullyPresent && !hasChanges) {
|
|
145
|
+
componentsToSkip.push(name);
|
|
146
|
+
}
|
|
147
|
+
else if (hasChanges) {
|
|
148
|
+
conflictingComponents.push(name);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
componentsToInstall.push(name);
|
|
117
152
|
}
|
|
118
153
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
checkSpinner.stop();
|
|
155
|
+
let componentsToOverwrite = [];
|
|
156
|
+
if (conflictingComponents.length > 0) {
|
|
157
|
+
if (options.overwrite) {
|
|
158
|
+
componentsToOverwrite = conflictingComponents;
|
|
159
|
+
}
|
|
160
|
+
else if (options.yes) {
|
|
161
|
+
componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
|
|
165
|
+
const { selected } = await prompts({
|
|
166
|
+
type: 'multiselect',
|
|
167
|
+
name: 'selected',
|
|
168
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
169
|
+
choices: conflictingComponents.map(name => ({
|
|
170
|
+
title: name,
|
|
171
|
+
value: name,
|
|
172
|
+
})),
|
|
173
|
+
hint: '- Space to select, Enter to confirm',
|
|
174
|
+
});
|
|
175
|
+
componentsToOverwrite = selected || [];
|
|
129
176
|
}
|
|
130
177
|
}
|
|
178
|
+
// Final list of components to process
|
|
179
|
+
// We process:
|
|
180
|
+
// 1. componentsToInstall (Brand new or partial)
|
|
181
|
+
// 2. componentsToOverwrite (User selected)
|
|
182
|
+
// We SKIP:
|
|
183
|
+
// 1. componentsToSkip (Identical)
|
|
184
|
+
// 2. conflictingComponents NOT in componentsToOverwrite
|
|
185
|
+
const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
|
|
186
|
+
if (finalComponents.length === 0 && componentsToSkip.length > 0) {
|
|
187
|
+
console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (finalComponents.length === 0) {
|
|
191
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
131
194
|
const spinner = ora('Installing components...').start();
|
|
132
195
|
let successCount = 0;
|
|
133
196
|
try {
|
|
134
197
|
await fs.ensureDir(targetDir);
|
|
135
|
-
for (const name of
|
|
198
|
+
for (const name of finalComponents) {
|
|
136
199
|
const component = registry[name];
|
|
137
200
|
let componentSuccess = true;
|
|
138
201
|
for (const file of component.files) {
|
|
139
202
|
const targetPath = path.join(targetDir, file);
|
|
140
203
|
try {
|
|
141
|
-
let content =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
let content = contentCache.get(file);
|
|
205
|
+
if (!content) {
|
|
206
|
+
content = await fetchComponentContent(file, options);
|
|
207
|
+
// Transform imports if not already transformed (cached is transformed)
|
|
208
|
+
const utilsAlias = config.aliases.utils;
|
|
209
|
+
content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
|
|
210
|
+
}
|
|
146
211
|
await fs.ensureDir(path.dirname(targetPath));
|
|
147
212
|
await fs.writeFile(targetPath, content);
|
|
148
|
-
spinner.text = `Added ${file}`;
|
|
213
|
+
// spinner.text = `Added ${file}`; // Too verbose?
|
|
149
214
|
}
|
|
150
215
|
catch (err) {
|
|
151
216
|
spinner.warn(`Could not add ${file}: ${err.message}`);
|
|
152
217
|
componentSuccess = false;
|
|
153
218
|
}
|
|
154
219
|
}
|
|
155
|
-
if (componentSuccess)
|
|
220
|
+
if (componentSuccess) {
|
|
156
221
|
successCount++;
|
|
222
|
+
spinner.text = `Added ${name}`;
|
|
223
|
+
}
|
|
157
224
|
}
|
|
158
225
|
if (successCount > 0) {
|
|
159
|
-
spinner.succeed(chalk.green(`Added ${successCount} component(s)`));
|
|
226
|
+
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
160
227
|
console.log('\n' + chalk.dim('Components added:'));
|
|
161
|
-
|
|
228
|
+
finalComponents.forEach(name => {
|
|
162
229
|
console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
163
230
|
});
|
|
164
|
-
console.log('');
|
|
165
231
|
}
|
|
166
232
|
else {
|
|
167
|
-
spinner.
|
|
233
|
+
spinner.info('No new components installed.');
|
|
234
|
+
}
|
|
235
|
+
if (finalComponents.length > 0) {
|
|
236
|
+
const npmDependencies = new Set();
|
|
237
|
+
for (const name of finalComponents) {
|
|
238
|
+
const component = registry[name];
|
|
239
|
+
if (component.npmDependencies) {
|
|
240
|
+
component.npmDependencies.forEach(dep => npmDependencies.add(dep));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (npmDependencies.size > 0) {
|
|
244
|
+
const depSpinner = ora('Installing dependencies...').start();
|
|
245
|
+
try {
|
|
246
|
+
await installPackages(Array.from(npmDependencies), { cwd });
|
|
247
|
+
depSpinner.succeed('Dependencies installed.');
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
depSpinner.fail('Failed to install dependencies.');
|
|
251
|
+
console.error(e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (componentsToSkip.length > 0) {
|
|
256
|
+
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
257
|
+
componentsToSkip.forEach(name => {
|
|
258
|
+
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
259
|
+
});
|
|
168
260
|
}
|
|
261
|
+
console.log('');
|
|
169
262
|
}
|
|
170
263
|
catch (error) {
|
|
171
264
|
spinner.fail('Failed to add components');
|
package/dist/commands/init.js
CHANGED
|
@@ -195,6 +195,35 @@ export async function init(options) {
|
|
|
195
195
|
};
|
|
196
196
|
await fs.writeJson(postcssrcPath, configContent, { spaces: 4 });
|
|
197
197
|
}
|
|
198
|
+
// Configure app.config.ts with Lucide icons
|
|
199
|
+
spinner.text = 'Configuring icons in app.config.ts...';
|
|
200
|
+
const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
|
|
201
|
+
if (await fs.pathExists(appConfigPath)) {
|
|
202
|
+
let appConfigContent = await fs.readFile(appConfigPath, 'utf-8');
|
|
203
|
+
// Add imports
|
|
204
|
+
if (!appConfigContent.includes('LucideAngularModule')) {
|
|
205
|
+
const iconImports = "import { LucideAngularModule, ArrowDown, ArrowUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-angular';";
|
|
206
|
+
appConfigContent = iconImports + '\n' + appConfigContent;
|
|
207
|
+
}
|
|
208
|
+
if (!appConfigContent.includes('importProvidersFrom')) {
|
|
209
|
+
appConfigContent = "import { importProvidersFrom } from '@angular/core';\n" + appConfigContent;
|
|
210
|
+
}
|
|
211
|
+
// Add provider
|
|
212
|
+
const providerCode = `
|
|
213
|
+
importProvidersFrom(LucideAngularModule.pick({
|
|
214
|
+
ArrowDown,
|
|
215
|
+
ArrowUp,
|
|
216
|
+
ChevronsUpDown,
|
|
217
|
+
ChevronLeft,
|
|
218
|
+
ChevronRight,
|
|
219
|
+
ChevronsLeft,
|
|
220
|
+
ChevronsRight
|
|
221
|
+
}))`;
|
|
222
|
+
if (!appConfigContent.includes('LucideAngularModule.pick')) {
|
|
223
|
+
appConfigContent = appConfigContent.replace(/providers:\s*\[/, `providers: [${providerCode},`);
|
|
224
|
+
await fs.writeFile(appConfigPath, appConfigContent);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
198
227
|
spinner.succeed(chalk.green('Project initialized successfully!'));
|
|
199
228
|
console.log('\n' + chalk.bold('Next steps:'));
|
|
200
229
|
console.log(chalk.dim(' 1. Add components: ') + chalk.cyan('npx @gilav21/shadcn-angular add button'));
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const program = new Command();
|
|
|
6
6
|
program
|
|
7
7
|
.name('shadcn-angular')
|
|
8
8
|
.description('CLI for adding shadcn-angular components to your Angular project')
|
|
9
|
-
.version('0.0.
|
|
9
|
+
.version('0.0.10');
|
|
10
10
|
program
|
|
11
11
|
.command('init')
|
|
12
12
|
.description('Initialize shadcn-angular in your project')
|
package/dist/registry/index.d.ts
CHANGED
package/dist/registry/index.js
CHANGED
|
@@ -89,6 +89,48 @@ export const registry = {
|
|
|
89
89
|
files: ['date-picker.component.ts'],
|
|
90
90
|
dependencies: ['calendar'],
|
|
91
91
|
},
|
|
92
|
+
chat: {
|
|
93
|
+
name: 'chat',
|
|
94
|
+
files: ['chat.component.ts'],
|
|
95
|
+
dependencies: ['avatar', 'button', 'textarea', 'scroll-area'],
|
|
96
|
+
},
|
|
97
|
+
'streaming-text': {
|
|
98
|
+
name: 'streaming-text',
|
|
99
|
+
files: ['streaming-text.component.ts'],
|
|
100
|
+
},
|
|
101
|
+
sparkles: {
|
|
102
|
+
name: 'sparkles',
|
|
103
|
+
files: ['sparkles.component.ts'],
|
|
104
|
+
dependencies: ['button'],
|
|
105
|
+
},
|
|
106
|
+
'code-block': {
|
|
107
|
+
name: 'code-block',
|
|
108
|
+
files: ['code-block.component.ts'],
|
|
109
|
+
dependencies: ['button', 'scroll-area'],
|
|
110
|
+
},
|
|
111
|
+
'text-reveal': {
|
|
112
|
+
name: 'text-reveal',
|
|
113
|
+
files: ['text-reveal.component.ts'],
|
|
114
|
+
},
|
|
115
|
+
'data-table': {
|
|
116
|
+
name: 'data-table',
|
|
117
|
+
files: [
|
|
118
|
+
'data-table/data-table.component.ts',
|
|
119
|
+
'data-table/data-table-column-header.component.ts',
|
|
120
|
+
'data-table/data-table-pagination.component.ts',
|
|
121
|
+
'data-table/data-table.types.ts',
|
|
122
|
+
'data-table/cell-host.directive.ts',
|
|
123
|
+
],
|
|
124
|
+
dependencies: [
|
|
125
|
+
'table',
|
|
126
|
+
'input',
|
|
127
|
+
'button',
|
|
128
|
+
'checkbox',
|
|
129
|
+
'select',
|
|
130
|
+
'pagination',
|
|
131
|
+
'popover',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
92
134
|
dialog: {
|
|
93
135
|
name: 'dialog',
|
|
94
136
|
files: ['dialog.component.ts'],
|
|
@@ -124,7 +166,8 @@ export const registry = {
|
|
|
124
166
|
},
|
|
125
167
|
'input-group': {
|
|
126
168
|
name: 'input-group',
|
|
127
|
-
files: ['input-group.component.ts'],
|
|
169
|
+
files: ['input-group.component.ts', 'input-group.token.ts'],
|
|
170
|
+
dependencies: ['input'],
|
|
128
171
|
},
|
|
129
172
|
'input-otp': {
|
|
130
173
|
name: 'input-otp',
|
|
@@ -197,6 +240,7 @@ export const registry = {
|
|
|
197
240
|
sidebar: {
|
|
198
241
|
name: 'sidebar',
|
|
199
242
|
files: ['sidebar.component.ts'],
|
|
243
|
+
dependencies: ['scroll-area', 'tooltip'],
|
|
200
244
|
},
|
|
201
245
|
skeleton: {
|
|
202
246
|
name: 'skeleton',
|
|
@@ -262,7 +306,7 @@ export const registry = {
|
|
|
262
306
|
'chip-list': {
|
|
263
307
|
name: 'chip-list',
|
|
264
308
|
files: ['chip-list.component.ts'],
|
|
265
|
-
dependencies: ['badge', 'button'],
|
|
309
|
+
dependencies: ['badge', 'button', 'input', 'input-group'],
|
|
266
310
|
},
|
|
267
311
|
'emoji-picker': {
|
|
268
312
|
name: 'emoji-picker',
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
2
|
+
export declare function getPackageManager(cwd: string): Promise<PackageManager>;
|
|
3
|
+
export declare function installPackages(packages: string[], options: {
|
|
4
|
+
cwd: string;
|
|
5
|
+
dev?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export async function getPackageManager(cwd) {
|
|
5
|
+
const userAgent = process.env['npm_config_user_agent'];
|
|
6
|
+
if (userAgent) {
|
|
7
|
+
if (userAgent.startsWith('yarn'))
|
|
8
|
+
return 'yarn';
|
|
9
|
+
if (userAgent.startsWith('pnpm'))
|
|
10
|
+
return 'pnpm';
|
|
11
|
+
if (userAgent.startsWith('bun'))
|
|
12
|
+
return 'bun';
|
|
13
|
+
return 'npm';
|
|
14
|
+
}
|
|
15
|
+
// Check for lock files
|
|
16
|
+
if (await fs.pathExists(path.join(cwd, 'yarn.lock')))
|
|
17
|
+
return 'yarn';
|
|
18
|
+
if (await fs.pathExists(path.join(cwd, 'pnpm-lock.yaml')))
|
|
19
|
+
return 'pnpm';
|
|
20
|
+
if (await fs.pathExists(path.join(cwd, 'bun.lockb')))
|
|
21
|
+
return 'bun';
|
|
22
|
+
return 'npm';
|
|
23
|
+
}
|
|
24
|
+
export async function installPackages(packages, options) {
|
|
25
|
+
const packageManager = await getPackageManager(options.cwd);
|
|
26
|
+
const args = [];
|
|
27
|
+
if (packageManager === 'npm') {
|
|
28
|
+
args.push('install');
|
|
29
|
+
if (options.dev)
|
|
30
|
+
args.push('-D');
|
|
31
|
+
}
|
|
32
|
+
else if (packageManager === 'yarn') {
|
|
33
|
+
args.push('add');
|
|
34
|
+
if (options.dev)
|
|
35
|
+
args.push('-D');
|
|
36
|
+
}
|
|
37
|
+
else if (packageManager === 'pnpm') {
|
|
38
|
+
args.push('add');
|
|
39
|
+
if (options.dev)
|
|
40
|
+
args.push('-D');
|
|
41
|
+
}
|
|
42
|
+
else if (packageManager === 'bun') {
|
|
43
|
+
args.push('add');
|
|
44
|
+
if (options.dev)
|
|
45
|
+
args.push('-d');
|
|
46
|
+
}
|
|
47
|
+
args.push(...packages);
|
|
48
|
+
await execa(packageManager, args, { cwd: options.cwd });
|
|
49
|
+
}
|
package/package.json
CHANGED
package/src/commands/add.ts
CHANGED
|
@@ -6,6 +6,7 @@ import chalk from 'chalk';
|
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import { getConfig } from '../utils/config.js';
|
|
8
8
|
import { registry, type ComponentName } from '../registry/index.js';
|
|
9
|
+
import { installPackages } from '../utils/package-manager.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -124,38 +125,106 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
124
125
|
? path.join(cwd, options.path)
|
|
125
126
|
: path.join(cwd, 'src/components/ui');
|
|
126
127
|
|
|
127
|
-
// Check for existing files
|
|
128
|
-
const
|
|
128
|
+
// Check for existing files and diff
|
|
129
|
+
const componentsToInstall: ComponentName[] = [];
|
|
130
|
+
const componentsToSkip: string[] = [];
|
|
131
|
+
const conflictingComponents: ComponentName[] = [];
|
|
132
|
+
const contentCache = new Map<string, string>();
|
|
133
|
+
|
|
134
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
135
|
+
|
|
129
136
|
for (const name of allComponents) {
|
|
130
137
|
const component = registry[name];
|
|
138
|
+
let hasChanges = false;
|
|
139
|
+
let isFullyPresent = true;
|
|
140
|
+
|
|
131
141
|
for (const file of component.files) {
|
|
132
142
|
const targetPath = path.join(targetDir, file);
|
|
133
143
|
if (await fs.pathExists(targetPath)) {
|
|
134
|
-
|
|
144
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
let remoteContent = await fetchComponentContent(file, options);
|
|
148
|
+
// Transform imports for comparison
|
|
149
|
+
const utilsAlias = config.aliases.utils;
|
|
150
|
+
remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
|
|
151
|
+
|
|
152
|
+
const normalize = (str: string) => str.replace(/\s+/g, '').trim();
|
|
153
|
+
if (normalize(localContent) !== normalize(remoteContent)) {
|
|
154
|
+
hasChanges = true;
|
|
155
|
+
}
|
|
156
|
+
contentCache.set(file, remoteContent); // Cache for installation
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// unexpected error fetching remote
|
|
159
|
+
console.warn(`Could not fetch remote content for comparison: ${file}`);
|
|
160
|
+
hasChanges = true; // Assume changed/unknown
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
isFullyPresent = false;
|
|
135
164
|
}
|
|
136
165
|
}
|
|
166
|
+
|
|
167
|
+
if (isFullyPresent && !hasChanges) {
|
|
168
|
+
componentsToSkip.push(name);
|
|
169
|
+
} else if (hasChanges) {
|
|
170
|
+
conflictingComponents.push(name);
|
|
171
|
+
} else {
|
|
172
|
+
componentsToInstall.push(name);
|
|
173
|
+
}
|
|
137
174
|
}
|
|
138
175
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
checkSpinner.stop();
|
|
177
|
+
|
|
178
|
+
let componentsToOverwrite: ComponentName[] = [];
|
|
179
|
+
|
|
180
|
+
if (conflictingComponents.length > 0) {
|
|
181
|
+
if (options.overwrite) {
|
|
182
|
+
componentsToOverwrite = conflictingComponents;
|
|
183
|
+
} else if (options.yes) {
|
|
184
|
+
componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
|
|
185
|
+
} else {
|
|
186
|
+
console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
|
|
187
|
+
const { selected } = await prompts({
|
|
188
|
+
type: 'multiselect',
|
|
189
|
+
name: 'selected',
|
|
190
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
191
|
+
choices: conflictingComponents.map(name => ({
|
|
192
|
+
title: name,
|
|
193
|
+
value: name,
|
|
194
|
+
})),
|
|
195
|
+
hint: '- Space to select, Enter to confirm',
|
|
196
|
+
});
|
|
197
|
+
componentsToOverwrite = selected || [];
|
|
149
198
|
}
|
|
150
199
|
}
|
|
151
200
|
|
|
201
|
+
// Final list of components to process
|
|
202
|
+
// We process:
|
|
203
|
+
// 1. componentsToInstall (Brand new or partial)
|
|
204
|
+
// 2. componentsToOverwrite (User selected)
|
|
205
|
+
// We SKIP:
|
|
206
|
+
// 1. componentsToSkip (Identical)
|
|
207
|
+
// 2. conflictingComponents NOT in componentsToOverwrite
|
|
208
|
+
|
|
209
|
+
const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
|
|
210
|
+
|
|
211
|
+
if (finalComponents.length === 0 && componentsToSkip.length > 0) {
|
|
212
|
+
console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (finalComponents.length === 0) {
|
|
217
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
152
221
|
const spinner = ora('Installing components...').start();
|
|
153
222
|
let successCount = 0;
|
|
154
223
|
|
|
155
224
|
try {
|
|
156
225
|
await fs.ensureDir(targetDir);
|
|
157
226
|
|
|
158
|
-
for (const name of
|
|
227
|
+
for (const name of finalComponents) {
|
|
159
228
|
const component = registry[name];
|
|
160
229
|
let componentSuccess = true;
|
|
161
230
|
|
|
@@ -163,35 +232,69 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
163
232
|
const targetPath = path.join(targetDir, file);
|
|
164
233
|
|
|
165
234
|
try {
|
|
166
|
-
let content =
|
|
235
|
+
let content = contentCache.get(file);
|
|
236
|
+
if (!content) {
|
|
237
|
+
content = await fetchComponentContent(file, options);
|
|
238
|
+
// Transform imports if not already transformed (cached is transformed)
|
|
239
|
+
const utilsAlias = config.aliases.utils;
|
|
240
|
+
content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
|
|
241
|
+
}
|
|
167
242
|
|
|
168
|
-
// Transform imports
|
|
169
|
-
// Replace ../lib/utils (or similar relative paths) with the configured alias
|
|
170
|
-
const utilsAlias = config.aliases.utils;
|
|
171
|
-
content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
|
|
172
243
|
await fs.ensureDir(path.dirname(targetPath));
|
|
173
244
|
await fs.writeFile(targetPath, content);
|
|
174
|
-
spinner.text = `Added ${file}`;
|
|
245
|
+
// spinner.text = `Added ${file}`; // Too verbose?
|
|
175
246
|
} catch (err: any) {
|
|
176
247
|
spinner.warn(`Could not add ${file}: ${err.message}`);
|
|
177
248
|
componentSuccess = false;
|
|
178
249
|
}
|
|
179
250
|
}
|
|
180
|
-
if (componentSuccess)
|
|
251
|
+
if (componentSuccess) {
|
|
252
|
+
successCount++;
|
|
253
|
+
spinner.text = `Added ${name}`;
|
|
254
|
+
}
|
|
181
255
|
}
|
|
182
256
|
|
|
183
257
|
if (successCount > 0) {
|
|
184
|
-
spinner.succeed(chalk.green(`Added ${successCount} component(s)`));
|
|
258
|
+
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
185
259
|
|
|
186
260
|
console.log('\n' + chalk.dim('Components added:'));
|
|
187
|
-
|
|
261
|
+
finalComponents.forEach(name => {
|
|
188
262
|
console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
189
263
|
});
|
|
190
|
-
console.log('');
|
|
191
264
|
} else {
|
|
192
|
-
spinner.
|
|
265
|
+
spinner.info('No new components installed.');
|
|
193
266
|
}
|
|
194
267
|
|
|
268
|
+
if (finalComponents.length > 0) {
|
|
269
|
+
const npmDependencies = new Set<string>();
|
|
270
|
+
for (const name of finalComponents) {
|
|
271
|
+
const component = registry[name];
|
|
272
|
+
if (component.npmDependencies) {
|
|
273
|
+
component.npmDependencies.forEach(dep => npmDependencies.add(dep));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (npmDependencies.size > 0) {
|
|
278
|
+
const depSpinner = ora('Installing dependencies...').start();
|
|
279
|
+
try {
|
|
280
|
+
await installPackages(Array.from(npmDependencies), { cwd });
|
|
281
|
+
depSpinner.succeed('Dependencies installed.');
|
|
282
|
+
} catch (e) {
|
|
283
|
+
depSpinner.fail('Failed to install dependencies.');
|
|
284
|
+
console.error(e);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (componentsToSkip.length > 0) {
|
|
290
|
+
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
291
|
+
componentsToSkip.forEach(name => {
|
|
292
|
+
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('');
|
|
297
|
+
|
|
195
298
|
} catch (error) {
|
|
196
299
|
spinner.fail('Failed to add components');
|
|
197
300
|
console.error(error);
|
package/src/commands/init.ts
CHANGED
|
@@ -224,6 +224,44 @@ export async function init(options: InitOptions) {
|
|
|
224
224
|
await fs.writeJson(postcssrcPath, configContent, { spaces: 4 });
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// Configure app.config.ts with Lucide icons
|
|
228
|
+
spinner.text = 'Configuring icons in app.config.ts...';
|
|
229
|
+
const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
|
|
230
|
+
|
|
231
|
+
if (await fs.pathExists(appConfigPath)) {
|
|
232
|
+
let appConfigContent = await fs.readFile(appConfigPath, 'utf-8');
|
|
233
|
+
|
|
234
|
+
// Add imports
|
|
235
|
+
if (!appConfigContent.includes('LucideAngularModule')) {
|
|
236
|
+
const iconImports = "import { LucideAngularModule, ArrowDown, ArrowUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-angular';";
|
|
237
|
+
appConfigContent = iconImports + '\n' + appConfigContent;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!appConfigContent.includes('importProvidersFrom')) {
|
|
241
|
+
appConfigContent = "import { importProvidersFrom } from '@angular/core';\n" + appConfigContent;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Add provider
|
|
245
|
+
const providerCode = `
|
|
246
|
+
importProvidersFrom(LucideAngularModule.pick({
|
|
247
|
+
ArrowDown,
|
|
248
|
+
ArrowUp,
|
|
249
|
+
ChevronsUpDown,
|
|
250
|
+
ChevronLeft,
|
|
251
|
+
ChevronRight,
|
|
252
|
+
ChevronsLeft,
|
|
253
|
+
ChevronsRight
|
|
254
|
+
}))`;
|
|
255
|
+
|
|
256
|
+
if (!appConfigContent.includes('LucideAngularModule.pick')) {
|
|
257
|
+
appConfigContent = appConfigContent.replace(
|
|
258
|
+
/providers:\s*\[/,
|
|
259
|
+
`providers: [${providerCode},`
|
|
260
|
+
);
|
|
261
|
+
await fs.writeFile(appConfigPath, appConfigContent);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
227
265
|
spinner.succeed(chalk.green('Project initialized successfully!'));
|
|
228
266
|
|
|
229
267
|
console.log('\n' + chalk.bold('Next steps:'));
|
package/src/index.ts
CHANGED
package/src/registry/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ComponentDefinition {
|
|
|
5
5
|
name: string;
|
|
6
6
|
files: string[]; // Relative paths to component files
|
|
7
7
|
dependencies?: string[]; // Other components this depends on
|
|
8
|
+
npmDependencies?: string[]; // NPM packages this depends on
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export type ComponentName = keyof typeof registry;
|
|
@@ -98,6 +99,48 @@ export const registry: Record<string, ComponentDefinition> = {
|
|
|
98
99
|
files: ['date-picker.component.ts'],
|
|
99
100
|
dependencies: ['calendar'],
|
|
100
101
|
},
|
|
102
|
+
chat: {
|
|
103
|
+
name: 'chat',
|
|
104
|
+
files: ['chat.component.ts'],
|
|
105
|
+
dependencies: ['avatar', 'button', 'textarea', 'scroll-area'],
|
|
106
|
+
},
|
|
107
|
+
'streaming-text': {
|
|
108
|
+
name: 'streaming-text',
|
|
109
|
+
files: ['streaming-text.component.ts'],
|
|
110
|
+
},
|
|
111
|
+
sparkles: {
|
|
112
|
+
name: 'sparkles',
|
|
113
|
+
files: ['sparkles.component.ts'],
|
|
114
|
+
dependencies: ['button'],
|
|
115
|
+
},
|
|
116
|
+
'code-block': {
|
|
117
|
+
name: 'code-block',
|
|
118
|
+
files: ['code-block.component.ts'],
|
|
119
|
+
dependencies: ['button', 'scroll-area'],
|
|
120
|
+
},
|
|
121
|
+
'text-reveal': {
|
|
122
|
+
name: 'text-reveal',
|
|
123
|
+
files: ['text-reveal.component.ts'],
|
|
124
|
+
},
|
|
125
|
+
'data-table': {
|
|
126
|
+
name: 'data-table',
|
|
127
|
+
files: [
|
|
128
|
+
'data-table/data-table.component.ts',
|
|
129
|
+
'data-table/data-table-column-header.component.ts',
|
|
130
|
+
'data-table/data-table-pagination.component.ts',
|
|
131
|
+
'data-table/data-table.types.ts',
|
|
132
|
+
'data-table/cell-host.directive.ts',
|
|
133
|
+
],
|
|
134
|
+
dependencies: [
|
|
135
|
+
'table',
|
|
136
|
+
'input',
|
|
137
|
+
'button',
|
|
138
|
+
'checkbox',
|
|
139
|
+
'select',
|
|
140
|
+
'pagination',
|
|
141
|
+
'popover',
|
|
142
|
+
],
|
|
143
|
+
},
|
|
101
144
|
dialog: {
|
|
102
145
|
name: 'dialog',
|
|
103
146
|
files: ['dialog.component.ts'],
|
|
@@ -118,6 +161,7 @@ export const registry: Record<string, ComponentDefinition> = {
|
|
|
118
161
|
name: 'field',
|
|
119
162
|
files: ['field.component.ts'],
|
|
120
163
|
},
|
|
164
|
+
|
|
121
165
|
'file-upload': {
|
|
122
166
|
name: 'file-upload',
|
|
123
167
|
files: ['file-upload.component.ts'],
|
|
@@ -133,7 +177,8 @@ export const registry: Record<string, ComponentDefinition> = {
|
|
|
133
177
|
},
|
|
134
178
|
'input-group': {
|
|
135
179
|
name: 'input-group',
|
|
136
|
-
files: ['input-group.component.ts'],
|
|
180
|
+
files: ['input-group.component.ts', 'input-group.token.ts'],
|
|
181
|
+
dependencies: ['input'],
|
|
137
182
|
},
|
|
138
183
|
'input-otp': {
|
|
139
184
|
name: 'input-otp',
|
|
@@ -206,6 +251,7 @@ export const registry: Record<string, ComponentDefinition> = {
|
|
|
206
251
|
sidebar: {
|
|
207
252
|
name: 'sidebar',
|
|
208
253
|
files: ['sidebar.component.ts'],
|
|
254
|
+
dependencies: ['scroll-area', 'tooltip'],
|
|
209
255
|
},
|
|
210
256
|
skeleton: {
|
|
211
257
|
name: 'skeleton',
|
|
@@ -271,7 +317,7 @@ export const registry: Record<string, ComponentDefinition> = {
|
|
|
271
317
|
'chip-list': {
|
|
272
318
|
name: 'chip-list',
|
|
273
319
|
files: ['chip-list.component.ts'],
|
|
274
|
-
dependencies: ['badge', 'button'],
|
|
320
|
+
dependencies: ['badge', 'button', 'input', 'input-group'],
|
|
275
321
|
},
|
|
276
322
|
'emoji-picker': {
|
|
277
323
|
name: 'emoji-picker',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
6
|
+
|
|
7
|
+
export async function getPackageManager(cwd: string): Promise<PackageManager> {
|
|
8
|
+
const userAgent = process.env['npm_config_user_agent'];
|
|
9
|
+
|
|
10
|
+
if (userAgent) {
|
|
11
|
+
if (userAgent.startsWith('yarn')) return 'yarn';
|
|
12
|
+
if (userAgent.startsWith('pnpm')) return 'pnpm';
|
|
13
|
+
if (userAgent.startsWith('bun')) return 'bun';
|
|
14
|
+
return 'npm';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Check for lock files
|
|
18
|
+
if (await fs.pathExists(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
19
|
+
if (await fs.pathExists(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
20
|
+
if (await fs.pathExists(path.join(cwd, 'bun.lockb'))) return 'bun';
|
|
21
|
+
|
|
22
|
+
return 'npm';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function installPackages(
|
|
26
|
+
packages: string[],
|
|
27
|
+
options: { cwd: string; dev?: boolean }
|
|
28
|
+
) {
|
|
29
|
+
const packageManager = await getPackageManager(options.cwd);
|
|
30
|
+
|
|
31
|
+
const args: string[] = [];
|
|
32
|
+
|
|
33
|
+
if (packageManager === 'npm') {
|
|
34
|
+
args.push('install');
|
|
35
|
+
if (options.dev) args.push('-D');
|
|
36
|
+
} else if (packageManager === 'yarn') {
|
|
37
|
+
args.push('add');
|
|
38
|
+
if (options.dev) args.push('-D');
|
|
39
|
+
} else if (packageManager === 'pnpm') {
|
|
40
|
+
args.push('add');
|
|
41
|
+
if (options.dev) args.push('-D');
|
|
42
|
+
} else if (packageManager === 'bun') {
|
|
43
|
+
args.push('add');
|
|
44
|
+
if (options.dev) args.push('-d');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
args.push(...packages);
|
|
48
|
+
|
|
49
|
+
await execa(packageManager, args, { cwd: options.cwd });
|
|
50
|
+
}
|