@gilav21/shadcn-angular 0.0.1

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.
@@ -0,0 +1,317 @@
1
+ const baseColors = {
2
+ neutral: {
3
+ light: {
4
+ '--background': 'oklch(1 0 0)',
5
+ '--foreground': 'oklch(0.145 0 0)',
6
+ '--card': 'oklch(1 0 0)',
7
+ '--card-foreground': 'oklch(0.145 0 0)',
8
+ '--popover': 'oklch(1 0 0)',
9
+ '--popover-foreground': 'oklch(0.145 0 0)',
10
+ '--primary': 'oklch(0.205 0 0)',
11
+ '--primary-foreground': 'oklch(0.985 0 0)',
12
+ '--secondary': 'oklch(0.97 0 0)',
13
+ '--secondary-foreground': 'oklch(0.205 0 0)',
14
+ '--muted': 'oklch(0.97 0 0)',
15
+ '--muted-foreground': 'oklch(0.556 0 0)',
16
+ '--accent': 'oklch(0.97 0 0)',
17
+ '--accent-foreground': 'oklch(0.205 0 0)',
18
+ '--destructive': 'oklch(0.577 0.245 27.325)',
19
+ '--border': 'oklch(0.922 0 0)',
20
+ '--input': 'oklch(0.922 0 0)',
21
+ '--ring': 'oklch(0.708 0 0)',
22
+ },
23
+ dark: {
24
+ '--background': 'oklch(0.145 0 0)',
25
+ '--foreground': 'oklch(0.985 0 0)',
26
+ '--card': 'oklch(0.205 0 0)',
27
+ '--card-foreground': 'oklch(0.985 0 0)',
28
+ '--popover': 'oklch(0.269 0 0)',
29
+ '--popover-foreground': 'oklch(0.985 0 0)',
30
+ '--primary': 'oklch(0.922 0 0)',
31
+ '--primary-foreground': 'oklch(0.205 0 0)',
32
+ '--secondary': 'oklch(0.269 0 0)',
33
+ '--secondary-foreground': 'oklch(0.985 0 0)',
34
+ '--muted': 'oklch(0.269 0 0)',
35
+ '--muted-foreground': 'oklch(0.708 0 0)',
36
+ '--accent': 'oklch(0.371 0 0)',
37
+ '--accent-foreground': 'oklch(0.985 0 0)',
38
+ '--destructive': 'oklch(0.704 0.191 22.216)',
39
+ '--border': 'oklch(1 0 0 / 10%)',
40
+ '--input': 'oklch(1 0 0 / 15%)',
41
+ '--ring': 'oklch(0.556 0 0)',
42
+ },
43
+ },
44
+ slate: {
45
+ light: {
46
+ '--background': 'oklch(1 0 0)',
47
+ '--foreground': 'oklch(0.129 0.042 264.695)',
48
+ '--card': 'oklch(1 0 0)',
49
+ '--card-foreground': 'oklch(0.129 0.042 264.695)',
50
+ '--popover': 'oklch(1 0 0)',
51
+ '--popover-foreground': 'oklch(0.129 0.042 264.695)',
52
+ '--primary': 'oklch(0.208 0.042 265.755)',
53
+ '--primary-foreground': 'oklch(0.984 0.003 247.858)',
54
+ '--secondary': 'oklch(0.968 0.007 247.896)',
55
+ '--secondary-foreground': 'oklch(0.208 0.042 265.755)',
56
+ '--muted': 'oklch(0.968 0.007 247.896)',
57
+ '--muted-foreground': 'oklch(0.554 0.046 257.417)',
58
+ '--accent': 'oklch(0.968 0.007 247.896)',
59
+ '--accent-foreground': 'oklch(0.208 0.042 265.755)',
60
+ '--destructive': 'oklch(0.577 0.245 27.325)',
61
+ '--border': 'oklch(0.929 0.013 255.508)',
62
+ '--input': 'oklch(0.929 0.013 255.508)',
63
+ '--ring': 'oklch(0.704 0.04 256.788)',
64
+ },
65
+ dark: {
66
+ '--background': 'oklch(0.129 0.042 264.695)',
67
+ '--foreground': 'oklch(0.984 0.003 247.858)',
68
+ '--card': 'oklch(0.208 0.042 265.755)',
69
+ '--card-foreground': 'oklch(0.984 0.003 247.858)',
70
+ '--popover': 'oklch(0.269 0.04 260.031)',
71
+ '--popover-foreground': 'oklch(0.984 0.003 247.858)',
72
+ '--primary': 'oklch(0.929 0.013 255.508)',
73
+ '--primary-foreground': 'oklch(0.208 0.042 265.755)',
74
+ '--secondary': 'oklch(0.269 0.04 260.031)',
75
+ '--secondary-foreground': 'oklch(0.984 0.003 247.858)',
76
+ '--muted': 'oklch(0.269 0.04 260.031)',
77
+ '--muted-foreground': 'oklch(0.704 0.04 256.788)',
78
+ '--accent': 'oklch(0.372 0.044 257.287)',
79
+ '--accent-foreground': 'oklch(0.984 0.003 247.858)',
80
+ '--destructive': 'oklch(0.704 0.191 22.216)',
81
+ '--border': 'oklch(1 0 0 / 10%)',
82
+ '--input': 'oklch(1 0 0 / 15%)',
83
+ '--ring': 'oklch(0.554 0.046 257.417)',
84
+ },
85
+ },
86
+ stone: {
87
+ light: {
88
+ '--background': 'oklch(1 0 0)',
89
+ '--foreground': 'oklch(0.147 0.004 49.25)',
90
+ '--card': 'oklch(1 0 0)',
91
+ '--card-foreground': 'oklch(0.147 0.004 49.25)',
92
+ '--popover': 'oklch(1 0 0)',
93
+ '--popover-foreground': 'oklch(0.147 0.004 49.25)',
94
+ '--primary': 'oklch(0.216 0.006 56.043)',
95
+ '--primary-foreground': 'oklch(0.985 0.001 106.423)',
96
+ '--secondary': 'oklch(0.97 0.001 106.424)',
97
+ '--secondary-foreground': 'oklch(0.216 0.006 56.043)',
98
+ '--muted': 'oklch(0.97 0.001 106.424)',
99
+ '--muted-foreground': 'oklch(0.553 0.013 58.071)',
100
+ '--accent': 'oklch(0.97 0.001 106.424)',
101
+ '--accent-foreground': 'oklch(0.216 0.006 56.043)',
102
+ '--destructive': 'oklch(0.577 0.245 27.325)',
103
+ '--border': 'oklch(0.923 0.003 48.717)',
104
+ '--input': 'oklch(0.923 0.003 48.717)',
105
+ '--ring': 'oklch(0.709 0.01 56.259)',
106
+ },
107
+ dark: {
108
+ '--background': 'oklch(0.147 0.004 49.25)',
109
+ '--foreground': 'oklch(0.985 0.001 106.423)',
110
+ '--card': 'oklch(0.216 0.006 56.043)',
111
+ '--card-foreground': 'oklch(0.985 0.001 106.423)',
112
+ '--popover': 'oklch(0.268 0.007 34.298)',
113
+ '--popover-foreground': 'oklch(0.985 0.001 106.423)',
114
+ '--primary': 'oklch(0.923 0.003 48.717)',
115
+ '--primary-foreground': 'oklch(0.216 0.006 56.043)',
116
+ '--secondary': 'oklch(0.268 0.007 34.298)',
117
+ '--secondary-foreground': 'oklch(0.985 0.001 106.423)',
118
+ '--muted': 'oklch(0.268 0.007 34.298)',
119
+ '--muted-foreground': 'oklch(0.709 0.01 56.259)',
120
+ '--accent': 'oklch(0.374 0.01 67.558)',
121
+ '--accent-foreground': 'oklch(0.985 0.001 106.423)',
122
+ '--destructive': 'oklch(0.704 0.191 22.216)',
123
+ '--border': 'oklch(1 0 0 / 10%)',
124
+ '--input': 'oklch(1 0 0 / 15%)',
125
+ '--ring': 'oklch(0.553 0.013 58.071)',
126
+ },
127
+ },
128
+ gray: {
129
+ light: {
130
+ '--background': 'oklch(1 0 0)',
131
+ '--foreground': 'oklch(0.13 0.028 261.692)',
132
+ '--card': 'oklch(1 0 0)',
133
+ '--card-foreground': 'oklch(0.13 0.028 261.692)',
134
+ '--popover': 'oklch(1 0 0)',
135
+ '--popover-foreground': 'oklch(0.13 0.028 261.692)',
136
+ '--primary': 'oklch(0.21 0.028 264.532)',
137
+ '--primary-foreground': 'oklch(0.985 0.002 247.839)',
138
+ '--secondary': 'oklch(0.967 0.003 264.542)',
139
+ '--secondary-foreground': 'oklch(0.21 0.028 264.532)',
140
+ '--muted': 'oklch(0.967 0.003 264.542)',
141
+ '--muted-foreground': 'oklch(0.551 0.027 264.364)',
142
+ '--accent': 'oklch(0.967 0.003 264.542)',
143
+ '--accent-foreground': 'oklch(0.21 0.028 264.532)',
144
+ '--destructive': 'oklch(0.577 0.245 27.325)',
145
+ '--border': 'oklch(0.928 0.006 264.531)',
146
+ '--input': 'oklch(0.928 0.006 264.531)',
147
+ '--ring': 'oklch(0.707 0.022 264.436)',
148
+ },
149
+ dark: {
150
+ '--background': 'oklch(0.13 0.028 261.692)',
151
+ '--foreground': 'oklch(0.985 0.002 247.839)',
152
+ '--card': 'oklch(0.21 0.028 264.532)',
153
+ '--card-foreground': 'oklch(0.985 0.002 247.839)',
154
+ '--popover': 'oklch(0.274 0.029 256.848)',
155
+ '--popover-foreground': 'oklch(0.985 0.002 247.839)',
156
+ '--primary': 'oklch(0.928 0.006 264.531)',
157
+ '--primary-foreground': 'oklch(0.21 0.028 264.532)',
158
+ '--secondary': 'oklch(0.274 0.029 256.848)',
159
+ '--secondary-foreground': 'oklch(0.985 0.002 247.839)',
160
+ '--muted': 'oklch(0.274 0.029 256.848)',
161
+ '--muted-foreground': 'oklch(0.707 0.022 264.436)',
162
+ '--accent': 'oklch(0.37 0.029 259.733)',
163
+ '--accent-foreground': 'oklch(0.985 0.002 247.839)',
164
+ '--destructive': 'oklch(0.704 0.191 22.216)',
165
+ '--border': 'oklch(1 0 0 / 10%)',
166
+ '--input': 'oklch(1 0 0 / 15%)',
167
+ '--ring': 'oklch(0.551 0.027 264.364)',
168
+ },
169
+ },
170
+ zinc: {
171
+ light: {
172
+ '--background': 'oklch(1 0 0)',
173
+ '--foreground': 'oklch(0.141 0.005 285.823)',
174
+ '--card': 'oklch(1 0 0)',
175
+ '--card-foreground': 'oklch(0.141 0.005 285.823)',
176
+ '--popover': 'oklch(1 0 0)',
177
+ '--popover-foreground': 'oklch(0.141 0.005 285.823)',
178
+ '--primary': 'oklch(0.21 0.006 285.885)',
179
+ '--primary-foreground': 'oklch(0.985 0 0)',
180
+ '--secondary': 'oklch(0.967 0.001 286.375)',
181
+ '--secondary-foreground': 'oklch(0.21 0.006 285.885)',
182
+ '--muted': 'oklch(0.967 0.001 286.375)',
183
+ '--muted-foreground': 'oklch(0.552 0.016 285.938)',
184
+ '--accent': 'oklch(0.967 0.001 286.375)',
185
+ '--accent-foreground': 'oklch(0.21 0.006 285.885)',
186
+ '--destructive': 'oklch(0.577 0.245 27.325)',
187
+ '--border': 'oklch(0.92 0.004 286.32)',
188
+ '--input': 'oklch(0.92 0.004 286.32)',
189
+ '--ring': 'oklch(0.705 0.015 286.067)',
190
+ },
191
+ dark: {
192
+ '--background': 'oklch(0.141 0.005 285.823)',
193
+ '--foreground': 'oklch(0.985 0 0)',
194
+ '--card': 'oklch(0.21 0.006 285.885)',
195
+ '--card-foreground': 'oklch(0.985 0 0)',
196
+ '--popover': 'oklch(0.274 0.006 286.033)',
197
+ '--popover-foreground': 'oklch(0.985 0 0)',
198
+ '--primary': 'oklch(0.92 0.004 286.32)',
199
+ '--primary-foreground': 'oklch(0.21 0.006 285.885)',
200
+ '--secondary': 'oklch(0.274 0.006 286.033)',
201
+ '--secondary-foreground': 'oklch(0.985 0 0)',
202
+ '--muted': 'oklch(0.274 0.006 286.033)',
203
+ '--muted-foreground': 'oklch(0.705 0.015 286.067)',
204
+ '--accent': 'oklch(0.37 0.013 285.805)',
205
+ '--accent-foreground': 'oklch(0.985 0 0)',
206
+ '--destructive': 'oklch(0.704 0.191 22.216)',
207
+ '--border': 'oklch(1 0 0 / 10%)',
208
+ '--input': 'oklch(1 0 0 / 15%)',
209
+ '--ring': 'oklch(0.552 0.016 285.938)',
210
+ },
211
+ },
212
+ };
213
+ function generateCssVars(vars, indent = ' ') {
214
+ return Object.entries(vars)
215
+ .map(([key, value]) => `${indent}${key}: ${value};`)
216
+ .join('\n');
217
+ }
218
+ export function getStylesTemplate(baseColor = 'neutral') {
219
+ const colors = baseColors[baseColor];
220
+ return `@import "tailwindcss";
221
+
222
+ @custom-variant dark (&:is(.dark *));
223
+
224
+ :root {
225
+ --radius: 0.625rem;
226
+ ${generateCssVars(colors.light)}
227
+ --chart-1: oklch(0.646 0.222 41.116);
228
+ --chart-2: oklch(0.6 0.118 184.704);
229
+ --chart-3: oklch(0.398 0.07 227.392);
230
+ --chart-4: oklch(0.828 0.189 84.429);
231
+ --chart-5: oklch(0.769 0.188 70.08);
232
+ --sidebar: oklch(0.985 0 0);
233
+ --sidebar-foreground: oklch(0.145 0 0);
234
+ --sidebar-primary: oklch(0.205 0 0);
235
+ --sidebar-primary-foreground: oklch(0.985 0 0);
236
+ --sidebar-accent: oklch(0.97 0 0);
237
+ --sidebar-accent-foreground: oklch(0.205 0 0);
238
+ --sidebar-border: oklch(0.922 0 0);
239
+ --sidebar-ring: oklch(0.708 0 0);
240
+ }
241
+
242
+ .dark {
243
+ ${generateCssVars(colors.dark)}
244
+ --chart-1: oklch(0.488 0.243 264.376);
245
+ --chart-2: oklch(0.696 0.17 162.48);
246
+ --chart-3: oklch(0.769 0.188 70.08);
247
+ --chart-4: oklch(0.627 0.265 303.9);
248
+ --chart-5: oklch(0.645 0.246 16.439);
249
+ --sidebar: oklch(0.205 0 0);
250
+ --sidebar-foreground: oklch(0.985 0 0);
251
+ --sidebar-primary: oklch(0.488 0.243 264.376);
252
+ --sidebar-primary-foreground: oklch(0.985 0 0);
253
+ --sidebar-accent: oklch(0.269 0 0);
254
+ --sidebar-accent-foreground: oklch(0.985 0 0);
255
+ --sidebar-border: oklch(1 0 0 / 10%);
256
+ --sidebar-ring: oklch(0.439 0 0);
257
+ }
258
+
259
+ @theme inline {
260
+ --font-sans: system-ui, sans-serif;
261
+ --color-background: var(--background);
262
+ --color-foreground: var(--foreground);
263
+ --color-card: var(--card);
264
+ --color-card-foreground: var(--card-foreground);
265
+ --color-popover: var(--popover);
266
+ --color-popover-foreground: var(--popover-foreground);
267
+ --color-primary: var(--primary);
268
+ --color-primary-foreground: var(--primary-foreground);
269
+ --color-secondary: var(--secondary);
270
+ --color-secondary-foreground: var(--secondary-foreground);
271
+ --color-muted: var(--muted);
272
+ --color-muted-foreground: var(--muted-foreground);
273
+ --color-accent: var(--accent);
274
+ --color-accent-foreground: var(--accent-foreground);
275
+ --color-destructive: var(--destructive);
276
+ --color-border: var(--border);
277
+ --color-input: var(--input);
278
+ --color-ring: var(--ring);
279
+ --color-chart-1: var(--chart-1);
280
+ --color-chart-2: var(--chart-2);
281
+ --color-chart-3: var(--chart-3);
282
+ --color-chart-4: var(--chart-4);
283
+ --color-chart-5: var(--chart-5);
284
+ --color-sidebar: var(--sidebar);
285
+ --color-sidebar-foreground: var(--sidebar-foreground);
286
+ --color-sidebar-primary: var(--sidebar-primary);
287
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
288
+ --color-sidebar-accent: var(--sidebar-accent);
289
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
290
+ --color-sidebar-border: var(--sidebar-border);
291
+ --color-sidebar-ring: var(--sidebar-ring);
292
+ --radius-sm: calc(var(--radius) - 4px);
293
+ --radius-md: calc(var(--radius) - 2px);
294
+ --radius-lg: var(--radius);
295
+ --radius-xl: calc(var(--radius) + 4px);
296
+ --radius-2xl: calc(var(--radius) + 8px);
297
+ --radius-3xl: calc(var(--radius) + 12px);
298
+ --radius-4xl: calc(var(--radius) + 16px);
299
+ }
300
+
301
+ @layer base {
302
+ * {
303
+ @apply border-border outline-ring/50;
304
+ }
305
+ body {
306
+ @apply font-sans bg-background text-foreground;
307
+ }
308
+ html {
309
+ @apply font-sans;
310
+ }
311
+ button:not(:disabled),
312
+ [role="button"]:not(:disabled) {
313
+ cursor: pointer;
314
+ }
315
+ }
316
+ `;
317
+ }
@@ -0,0 +1 @@
1
+ export declare function getUtilsTemplate(): string;
@@ -0,0 +1,12 @@
1
+ export function getUtilsTemplate() {
2
+ return `import { clsx, type ClassValue } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ /**
6
+ * Utility function for merging Tailwind CSS classes with proper precedence
7
+ */
8
+ export function cn(...inputs: ClassValue[]): string {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+ `;
12
+ }
@@ -0,0 +1,18 @@
1
+ export interface Config {
2
+ $schema: string;
3
+ style: 'default' | 'new-york';
4
+ tailwind: {
5
+ css: string;
6
+ baseColor: 'neutral' | 'slate' | 'stone' | 'gray' | 'zinc';
7
+ cssVariables: boolean;
8
+ };
9
+ aliases: {
10
+ components: string;
11
+ utils: string;
12
+ ui: string;
13
+ };
14
+ iconLibrary: string;
15
+ }
16
+ export declare function getDefaultConfig(): Config;
17
+ export declare function getConfig(cwd: string): Promise<Config | null>;
18
+ export declare function writeConfig(cwd: string, config: Config): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ export function getDefaultConfig() {
4
+ return {
5
+ $schema: 'https://shadcn-angular.dev/schema.json',
6
+ style: 'default',
7
+ tailwind: {
8
+ css: 'src/styles.scss',
9
+ baseColor: 'neutral',
10
+ cssVariables: true,
11
+ },
12
+ aliases: {
13
+ components: '@/components',
14
+ utils: '@/components/lib/utils',
15
+ ui: '@/components/ui',
16
+ },
17
+ iconLibrary: 'lucide-angular',
18
+ };
19
+ }
20
+ export async function getConfig(cwd) {
21
+ const configPath = path.join(cwd, 'components.json');
22
+ if (!await fs.pathExists(configPath)) {
23
+ return null;
24
+ }
25
+ try {
26
+ return await fs.readJson(configPath);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export async function writeConfig(cwd, config) {
33
+ const configPath = path.join(cwd, 'components.json');
34
+ await fs.writeJson(configPath, config, { spaces: 2 });
35
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@gilav21/shadcn-angular",
3
+ "version": "0.0.1",
4
+ "description": "CLI for adding shadcn-angular components to your project",
5
+ "bin": {
6
+ "shadcn-angular": "./dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "angular",
17
+ "shadcn",
18
+ "cli",
19
+ "components"
20
+ ],
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "fs-extra": "^11.2.0",
25
+ "ora": "^8.0.1",
26
+ "prompts": "^2.4.2",
27
+ "execa": "^9.3.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/fs-extra": "^11.0.4",
31
+ "@types/node": "^20.14.0",
32
+ "@types/prompts": "^2.4.9",
33
+ "typescript": "^5.5.0"
34
+ }
35
+ }
@@ -0,0 +1,187 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import prompts from 'prompts';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { registry, type ComponentName } from '../registry/index.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // Base URL for the component registry (GitHub raw content)
14
+ const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/main/packages/components/ui';
15
+
16
+ // Components source directory (relative to CLI dist folder) for local dev
17
+ function getLocalComponentsDir(): string | null {
18
+ // From dist/commands/add.js -> packages/components/ui
19
+ const fromDist = path.resolve(__dirname, '../../../components/ui');
20
+ if (fs.existsSync(fromDist)) {
21
+ return fromDist;
22
+ }
23
+ // Fallback: from src/commands/add.ts -> packages/components/ui
24
+ const fromSrc = path.resolve(__dirname, '../../../components/ui');
25
+ if (fs.existsSync(fromSrc)) {
26
+ return fromSrc;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ interface AddOptions {
32
+ yes?: boolean;
33
+ overwrite?: boolean;
34
+ all?: boolean;
35
+ path?: string;
36
+ remote?: boolean; // Force remote fetch
37
+ }
38
+
39
+ async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
40
+ const localDir = getLocalComponentsDir();
41
+
42
+ // 1. Prefer local if available and not forced remote
43
+ if (localDir && !options.remote) {
44
+ const localPath = path.join(localDir, file);
45
+ if (await fs.pathExists(localPath)) {
46
+ return fs.readFile(localPath, 'utf-8');
47
+ }
48
+ }
49
+
50
+ // 2. Fetch from remote registry
51
+ const url = `${REGISTRY_BASE_URL}/${file}`;
52
+ try {
53
+ const response = await fetch(url);
54
+ if (!response.ok) {
55
+ throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
56
+ }
57
+ return await response.text();
58
+ } catch (error) {
59
+ if (localDir) {
60
+ throw new Error(`Component file not found locally or remotely: ${file}`);
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ export async function add(components: string[], options: AddOptions) {
67
+ const cwd = process.cwd();
68
+
69
+ // Load config
70
+ const config = await getConfig(cwd);
71
+ if (!config) {
72
+ console.log(chalk.red('Error: components.json not found.'));
73
+ console.log(chalk.dim('Run `npx shadcn-angular init` first.'));
74
+ process.exit(1);
75
+ }
76
+
77
+ // Get components to add
78
+ let componentsToAdd: ComponentName[] = [];
79
+
80
+ if (options.all) {
81
+ componentsToAdd = Object.keys(registry) as ComponentName[];
82
+ } else if (components.length === 0) {
83
+ const { selected } = await prompts({
84
+ type: 'multiselect',
85
+ name: 'selected',
86
+ message: 'Which components would you like to add?',
87
+ choices: Object.keys(registry).map(name => ({
88
+ title: name,
89
+ value: name,
90
+ })),
91
+ hint: '- Space to select, Enter to confirm',
92
+ });
93
+ componentsToAdd = selected;
94
+ } else {
95
+ componentsToAdd = components as ComponentName[];
96
+ }
97
+
98
+ if (!componentsToAdd || componentsToAdd.length === 0) {
99
+ console.log(chalk.dim('No components selected.'));
100
+ return;
101
+ }
102
+
103
+ // Validate components exist
104
+ const invalidComponents = componentsToAdd.filter(c => !registry[c]);
105
+ if (invalidComponents.length > 0) {
106
+ console.log(chalk.red(`Invalid component(s): ${invalidComponents.join(', ')}`));
107
+ console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
108
+ process.exit(1);
109
+ }
110
+
111
+ // Resolve dependencies
112
+ const allComponents = new Set<ComponentName>();
113
+ const resolveDeps = (name: ComponentName) => {
114
+ if (allComponents.has(name)) return;
115
+ allComponents.add(name);
116
+ const component = registry[name];
117
+ if (component.dependencies) {
118
+ component.dependencies.forEach(dep => resolveDeps(dep as ComponentName));
119
+ }
120
+ };
121
+ componentsToAdd.forEach(c => resolveDeps(c));
122
+
123
+ const targetDir = options.path
124
+ ? path.join(cwd, options.path)
125
+ : path.join(cwd, 'src/components/ui');
126
+
127
+ // Check for existing files
128
+ const existing: string[] = [];
129
+ for (const name of allComponents) {
130
+ const component = registry[name];
131
+ for (const file of component.files) {
132
+ const targetPath = path.join(targetDir, file);
133
+ if (await fs.pathExists(targetPath)) {
134
+ existing.push(file);
135
+ }
136
+ }
137
+ }
138
+
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;
149
+ }
150
+ }
151
+
152
+ const spinner = ora('Installing components...').start();
153
+
154
+ try {
155
+ await fs.ensureDir(targetDir);
156
+
157
+ for (const name of allComponents) {
158
+ const component = registry[name];
159
+
160
+ for (const file of component.files) {
161
+ const targetPath = path.join(targetDir, file);
162
+
163
+ try {
164
+ const content = await fetchComponentContent(file, options);
165
+ await fs.ensureDir(path.dirname(targetPath));
166
+ await fs.writeFile(targetPath, content);
167
+ spinner.text = `Added ${file}`;
168
+ } catch (err: any) {
169
+ spinner.warn(`Could not add ${file}: ${err.message}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ spinner.succeed(chalk.green(`Added ${allComponents.size} component(s)`));
175
+
176
+ console.log('\n' + chalk.dim('Components added:'));
177
+ allComponents.forEach(name => {
178
+ console.log(chalk.dim(' - ') + chalk.cyan(name));
179
+ });
180
+ console.log('');
181
+
182
+ } catch (error) {
183
+ spinner.fail('Failed to add components');
184
+ console.error(error);
185
+ process.exit(1);
186
+ }
187
+ }