@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.
@@ -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 existing = [];
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
- existing.push(file);
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
- if (existing.length > 0 && !options.overwrite && !options.yes) {
120
- const { overwrite } = await prompts({
121
- type: 'confirm',
122
- name: 'overwrite',
123
- message: `The following files already exist: ${existing.join(', ')}. Overwrite?`,
124
- initial: false,
125
- });
126
- if (!overwrite) {
127
- console.log(chalk.dim('Installation cancelled.'));
128
- return;
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 allComponents) {
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 = await fetchComponentContent(file, options);
142
- // Transform imports
143
- // Replace ../lib/utils (or similar relative paths) with the configured alias
144
- const utilsAlias = config.aliases.utils;
145
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
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
- allComponents.forEach(name => {
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.fail(chalk.red('Failed to add any components.'));
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');
@@ -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.8');
9
+ .version('0.0.10');
10
10
  program
11
11
  .command('init')
12
12
  .description('Initialize shadcn-angular in your project')
@@ -2,6 +2,7 @@ export interface ComponentDefinition {
2
2
  name: string;
3
3
  files: string[];
4
4
  dependencies?: string[];
5
+ npmDependencies?: string[];
5
6
  }
6
7
  export type ComponentName = keyof typeof registry;
7
8
  export declare const registry: Record<string, ComponentDefinition>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"
@@ -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 existing: string[] = [];
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
- existing.push(file);
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
- if (existing.length > 0 && !options.overwrite && !options.yes) {
140
- const { overwrite } = await prompts({
141
- type: 'confirm',
142
- name: 'overwrite',
143
- message: `The following files already exist: ${existing.join(', ')}. Overwrite?`,
144
- initial: false,
145
- });
146
- if (!overwrite) {
147
- console.log(chalk.dim('Installation cancelled.'));
148
- return;
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 allComponents) {
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 = await fetchComponentContent(file, options);
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) successCount++;
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
- allComponents.forEach(name => {
261
+ finalComponents.forEach(name => {
188
262
  console.log(chalk.dim(' - ') + chalk.cyan(name));
189
263
  });
190
- console.log('');
191
264
  } else {
192
- spinner.fail(chalk.red('Failed to add any components.'));
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);
@@ -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
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name('shadcn-angular')
10
10
  .description('CLI for adding shadcn-angular components to your Angular project')
11
- .version('0.0.8');
11
+ .version('0.0.10');
12
12
 
13
13
  program
14
14
  .command('init')
@@ -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
+ }