@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.
@@ -0,0 +1,66 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { getConfig } from '../utils/config.js';
5
+ import { registry, getComponentNames } from '../registry/index.js';
6
+ import { resolveProjectPath, aliasToProjectPath } from '../utils/paths.js';
7
+
8
+ type Status = 'installed' | 'not installed';
9
+
10
+ interface ComponentStatus {
11
+ readonly name: string;
12
+ readonly status: Status;
13
+ }
14
+
15
+ export async function list() {
16
+ const cwd = process.cwd();
17
+
18
+ const config = await getConfig(cwd);
19
+ if (!config) {
20
+ console.log(chalk.red('Error: components.json not found.'));
21
+ console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const uiBasePath = aliasToProjectPath(config.aliases.ui || 'src/components/ui');
26
+ const targetDir = resolveProjectPath(cwd, uiBasePath);
27
+
28
+ const statuses: ComponentStatus[] = [];
29
+
30
+ for (const name of getComponentNames()) {
31
+ const component = registry[name];
32
+ const allPresent = await allFilesExist(component.files, targetDir);
33
+ statuses.push({
34
+ name,
35
+ status: allPresent ? 'installed' : 'not installed',
36
+ });
37
+ }
38
+
39
+ const installed = statuses.filter(s => s.status === 'installed').sort((a, b) => a.name.localeCompare(b.name));
40
+ const notInstalled = statuses.filter(s => s.status === 'not installed').sort((a, b) => a.name.localeCompare(b.name));
41
+
42
+ console.log(chalk.bold(`\nInstalled (${installed.length}):`));
43
+ if (installed.length === 0) {
44
+ console.log(chalk.dim(' None'));
45
+ } else {
46
+ for (const s of installed) {
47
+ console.log(chalk.green(' ✓ ') + s.name);
48
+ }
49
+ }
50
+
51
+ console.log(chalk.bold(`\nAvailable (${notInstalled.length}):`));
52
+ for (const s of notInstalled) {
53
+ console.log(chalk.dim(' - ') + s.name);
54
+ }
55
+
56
+ console.log('');
57
+ }
58
+
59
+ async function allFilesExist(files: readonly string[], targetDir: string): Promise<boolean> {
60
+ for (const file of files) {
61
+ if (!await fs.pathExists(path.join(targetDir, file))) {
62
+ return false;
63
+ }
64
+ }
65
+ return true;
66
+ }
package/src/index.ts CHANGED
@@ -1,22 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { Command } from 'commander';
3
4
  import { init } from './commands/init.js';
4
5
  import { add } from './commands/add.js';
6
+ import { diff } from './commands/diff.js';
7
+ import { list } from './commands/list.js';
5
8
  import { help } from './commands/help.js';
6
9
 
10
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')) as { version: string };
11
+
7
12
  const program = new Command();
8
13
 
9
14
  program
10
15
  .name('shadcn-angular')
11
16
  .description('CLI for adding shadcn-angular components to your Angular project')
12
- .version('0.0.10');
17
+ .version(pkg.version);
13
18
 
14
19
  program
15
20
  .command('init')
16
21
  .description('Initialize shadcn-angular in your project')
17
22
  .option('-y, --yes', 'Skip confirmation prompt')
18
23
  .option('-d, --defaults', 'Use default configuration')
24
+ .option('--remote', 'Force remote fetch from GitHub registry')
19
25
  .option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
26
+ .option('-r, --registry <url>', 'Custom registry base URL (e.g., https://gitlab.com/org/repo/-/raw/main/packages/components)')
20
27
  .action(init);
21
28
 
22
29
  program
@@ -28,9 +35,25 @@ program
28
35
  .option('-a, --all', 'Add all available components')
29
36
  .option('-p, --path <path>', 'The path to add the component to')
30
37
  .option('--remote', 'Force remote fetch from GitHub registry')
38
+ .option('--dry-run', 'Show what would be installed without making changes')
31
39
  .option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
40
+ .option('-r, --registry <url>', 'Custom registry base URL (overrides components.json)')
32
41
  .action(add);
33
42
 
43
+ program
44
+ .command('diff')
45
+ .description('Show differences between local and remote component versions')
46
+ .argument('[components...]', 'Components to diff (all installed if omitted)')
47
+ .option('--remote', 'Force remote fetch from GitHub registry')
48
+ .option('-b, --branch <branch>', 'GitHub branch to fetch from', 'master')
49
+ .option('-r, --registry <url>', 'Custom registry base URL')
50
+ .action(diff);
51
+
52
+ program
53
+ .command('list')
54
+ .description('List all components and their install status')
55
+ .action(list);
56
+
34
57
  program
35
58
  .command('help')
36
59
  .description('Show detailed usage information')
@@ -7,25 +7,25 @@ export interface OptionalDependency {
7
7
  }
8
8
 
9
9
  export interface ComponentDefinition {
10
- name: string;
11
- files: string[]; // Relative paths to component files
12
- peerFiles?: string[]; // Files to update only if they already exist in the user's project
13
- dependencies?: string[]; // Other components this depends on
14
- optionalDependencies?: readonly OptionalDependency[]; // Companion components offered during install
15
- npmDependencies?: string[]; // NPM packages this depends on
16
- libFiles?: string[]; // Lib utility files this component requires (e.g. 'xlsx.ts')
17
- shortcutDefinitions?: {
18
- exportName: string;
19
- componentName: string;
20
- sourceFile: string;
10
+ readonly name: string;
11
+ readonly files: readonly string[];
12
+ readonly peerFiles?: readonly string[];
13
+ readonly dependencies?: readonly string[];
14
+ readonly optionalDependencies?: readonly OptionalDependency[];
15
+ readonly npmDependencies?: readonly string[];
16
+ readonly libFiles?: readonly string[];
17
+ readonly shortcutDefinitions?: readonly {
18
+ readonly exportName: string;
19
+ readonly componentName: string;
20
+ readonly sourceFile: string;
21
21
  }[];
22
22
  }
23
23
 
24
- export type ComponentName = keyof typeof registry;
24
+ function defineRegistry<T extends Record<string, ComponentDefinition>>(reg: T): { readonly [K in keyof T]: ComponentDefinition } {
25
+ return reg;
26
+ }
25
27
 
26
- // Registry maps component names to their file definitions
27
- // Files are relative to the components/ui directory
28
- export const registry: Record<string, ComponentDefinition> = {
28
+ export const registry = defineRegistry({
29
29
  accordion: {
30
30
  name: 'accordion',
31
31
  files: ['accordion.component.ts'],
@@ -33,7 +33,7 @@ export const registry: Record<string, ComponentDefinition> = {
33
33
  autocomplete: {
34
34
  name: 'autocomplete',
35
35
  files: ['autocomplete.component.ts', 'highlight.pipe.ts'],
36
- dependencies: ['popover', 'command', 'badge'],
36
+ dependencies: ['badge', 'command', 'popover'],
37
37
  },
38
38
  alert: {
39
39
  name: 'alert',
@@ -66,12 +66,11 @@ export const registry: Record<string, ComponentDefinition> = {
66
66
  },
67
67
  'button-group': {
68
68
  name: 'button-group',
69
- files: ['button-group.component.ts'],
70
- dependencies: ['button']
69
+ files: ['button-group.component.ts']
71
70
  },
72
71
  calendar: {
73
72
  name: 'calendar',
74
- files: ['calendar.component.ts', 'calendar-locales.ts'],
73
+ files: ['calendar-locales.ts', 'calendar.component.ts'],
75
74
  dependencies: ['button', 'select'],
76
75
  },
77
76
  card: {
@@ -93,7 +92,7 @@ export const registry: Record<string, ComponentDefinition> = {
93
92
  'color-picker': {
94
93
  name: 'color-picker',
95
94
  files: ['color-picker.component.ts'],
96
- dependencies: ['popover', 'input', 'tabs'],
95
+ dependencies: ['input', 'popover', 'tabs'],
97
96
  },
98
97
  confetti: {
99
98
  name: 'confetti',
@@ -114,7 +113,7 @@ export const registry: Record<string, ComponentDefinition> = {
114
113
  },
115
114
  'context-menu': {
116
115
  name: 'context-menu',
117
- files: ['context-menu.component.ts', 'context-menu-integrations.ts'],
116
+ files: ['context-menu.component.ts'],
118
117
  },
119
118
  'date-picker': {
120
119
  name: 'date-picker',
@@ -124,7 +123,7 @@ export const registry: Record<string, ComponentDefinition> = {
124
123
  chat: {
125
124
  name: 'chat',
126
125
  files: ['chat.component.ts'],
127
- dependencies: ['avatar', 'button', 'textarea', 'scroll-area'],
126
+ dependencies: ['avatar', 'button', 'scroll-area', 'textarea'],
128
127
  },
129
128
  'streaming-text': {
130
129
  name: 'streaming-text',
@@ -138,7 +137,7 @@ export const registry: Record<string, ComponentDefinition> = {
138
137
  'code-block': {
139
138
  name: 'code-block',
140
139
  files: ['code-block.component.ts'],
141
- dependencies: ['button', 'scroll-area'],
140
+ dependencies: ['button'],
142
141
  },
143
142
  'text-reveal': {
144
143
  name: 'text-reveal',
@@ -146,31 +145,11 @@ export const registry: Record<string, ComponentDefinition> = {
146
145
  },
147
146
  'data-table': {
148
147
  name: 'data-table',
149
- files: [
150
- 'data-table/data-table.component.ts',
151
- 'data-table/data-table-column-header.component.ts',
152
- 'data-table/data-table-pagination.component.ts',
153
- 'data-table/data-table-multiselect-filter.component.ts',
154
- 'data-table/data-table.types.ts',
155
- 'data-table/data-table.utils.ts',
156
- 'data-table/index.ts',
157
- ],
148
+ files: ['calendar-locales.ts', 'data-table/component-pool.service.ts', 'data-table/data-table-column-header.component.ts', 'data-table/data-table-date-filter.component.ts', 'data-table/data-table-multiselect-filter.component.ts', 'data-table/data-table-pagination.component.ts', 'data-table/data-table.component.ts', 'data-table/data-table.types.ts', 'data-table/data-table.utils.ts', 'data-table/index.ts'],
158
149
  peerFiles: [
159
150
  'context-menu-integrations.ts',
160
151
  ],
161
- dependencies: [
162
- 'table',
163
- 'input',
164
- 'button',
165
- 'checkbox',
166
- 'select',
167
- 'pagination',
168
- 'popover',
169
- 'component-outlet',
170
- 'icon',
171
- 'command',
172
- 'badge',
173
- ],
152
+ dependencies: ['badge', 'button', 'calendar', 'checkbox', 'command', 'component-outlet', 'context-menu', 'icon', 'input', 'pagination', 'popover', 'select', 'table'],
174
153
  libFiles: ['xlsx.ts'],
175
154
  optionalDependencies: [
176
155
  { name: 'context-menu', description: 'Enables right-click context menus on rows and headers' },
@@ -182,18 +161,12 @@ export const registry: Record<string, ComponentDefinition> = {
182
161
  },
183
162
  dock: {
184
163
  name: 'dock',
185
- files: [
186
- 'dock.component.ts',
187
- 'dock-item.component.ts',
188
- 'dock-icon.component.ts',
189
- 'dock-label.component.ts',
190
- ],
191
- dependencies: ['icon'],
164
+ files: ['dock-icon.component.ts', 'dock-item.component.ts', 'dock-label.component.ts', 'dock.component.ts'],
192
165
  },
193
166
  'tree-select': {
194
167
  name: 'tree-select',
195
168
  files: ['tree-select.component.ts'],
196
- dependencies: ['popover', 'tree', 'icon'],
169
+ dependencies: ['popover', 'tree'],
197
170
  },
198
171
  'virtual-scroll': {
199
172
  name: 'virtual-scroll',
@@ -222,7 +195,7 @@ export const registry: Record<string, ComponentDefinition> = {
222
195
  },
223
196
  icon: {
224
197
  name: 'icon',
225
- files: ['icon.component.ts'],
198
+ files: ['icon.component.ts', 'icon.token.ts'],
226
199
  },
227
200
 
228
201
  'file-upload': {
@@ -234,19 +207,7 @@ export const registry: Record<string, ComponentDefinition> = {
234
207
  name: 'file-viewer',
235
208
  files: ['file-viewer.component.ts'],
236
209
  dependencies: ['spinner'],
237
- libFiles: [
238
- 'file-type-detector.ts',
239
- 'inflate.ts',
240
- 'zip-reader.ts',
241
- 'image-validator.ts',
242
- 'ole2-reader.ts',
243
- 'pptx-parser.ts',
244
- 'xlsx-reader.ts',
245
- 'docx-parser.ts',
246
- 'doc-enhanced-parser.ts',
247
- 'ppt-parser.ts',
248
- 'svg-sanitizer.ts',
249
- ],
210
+ libFiles: ['doc-enhanced-parser.ts', 'docx-parser.ts', 'file-type-detector.ts', 'image-validator.ts', 'inflate.ts', 'ole2-reader.ts', 'ppt-parser.ts', 'pptx-parser.ts', 'svg-sanitizer.ts', 'xlsx-reader.ts', 'zip-reader.ts'],
250
211
  },
251
212
  'hover-card': {
252
213
  name: 'hover-card',
@@ -254,12 +215,11 @@ export const registry: Record<string, ComponentDefinition> = {
254
215
  },
255
216
  input: {
256
217
  name: 'input',
257
- files: ['input.component.ts', 'input-group.token.ts'],
218
+ files: ['input-group.token.ts', 'input.component.ts'],
258
219
  },
259
220
  'input-group': {
260
221
  name: 'input-group',
261
222
  files: ['input-group.component.ts', 'input-group.token.ts'],
262
- dependencies: ['input'],
263
223
  },
264
224
  'input-otp': {
265
225
  name: 'input-otp',
@@ -332,7 +292,7 @@ export const registry: Record<string, ComponentDefinition> = {
332
292
  sidebar: {
333
293
  name: 'sidebar',
334
294
  files: ['sidebar.component.ts'],
335
- dependencies: ['scroll-area', 'tooltip', 'icon'],
295
+ dependencies: ['scroll-area', 'tooltip'],
336
296
  },
337
297
  skeleton: {
338
298
  name: 'skeleton',
@@ -364,7 +324,7 @@ export const registry: Record<string, ComponentDefinition> = {
364
324
  },
365
325
  textarea: {
366
326
  name: 'textarea',
367
- files: ['textarea.component.ts', 'input-group.token.ts'],
327
+ files: ['input-group.token.ts', 'textarea.component.ts'],
368
328
  },
369
329
  timeline: {
370
330
  name: 'timeline',
@@ -389,60 +349,29 @@ export const registry: Record<string, ComponentDefinition> = {
389
349
  tree: {
390
350
  name: 'tree',
391
351
  files: ['tree.component.ts'],
392
- dependencies: ['icon'],
393
352
  optionalDependencies: [
394
353
  { name: 'context-menu', description: 'Enables right-click context menus on tree nodes' },
395
354
  ],
396
355
  },
397
356
  'speed-dial': {
398
357
  name: 'speed-dial',
399
- files: ['speed-dial.component.ts'],
400
- dependencies: ['button']
358
+ files: ['speed-dial.component.ts']
401
359
  },
402
360
  'chip-list': {
403
361
  name: 'chip-list',
404
- files: ['chip-list.component.ts'],
405
- dependencies: ['badge', 'button', 'input', 'input-group'],
362
+ files: ['chip-list.component.ts', 'input-group.token.ts'],
363
+ dependencies: ['badge', 'button', 'input'],
406
364
  },
407
365
  'emoji-picker': {
408
366
  name: 'emoji-picker',
409
- files: ['emoji-picker.component.ts', 'emoji-data.ts'],
367
+ files: ['emoji-data.ts', 'emoji-picker.component.ts'],
410
368
  dependencies: ['input', 'scroll-area', 'tooltip'],
411
369
  },
412
370
  'rich-text-editor': {
413
371
  name: 'rich-text-editor',
414
- files: [
415
- 'rich-text-editor.component.ts',
416
- 'rich-text-toolbar.component.ts',
417
- 'rich-text-sanitizer.service.ts',
418
- 'rich-text-markdown.service.ts',
419
- 'rich-text-paste-normalizer.service.ts',
420
- 'rich-text-command-registry.service.ts',
421
- 'rich-text-mention.component.ts',
422
- 'rich-text-image-resizer.component.ts',
423
- 'rich-text-locales.ts',
424
- ],
425
- dependencies: [
426
- 'button',
427
- 'separator',
428
- 'popover',
429
- 'emoji-picker',
430
- 'autocomplete',
431
- 'select',
432
- 'input',
433
- 'dialog',
434
- 'scroll-area',
435
- ],
436
- libFiles: [
437
- 'pdf-parser.ts',
438
- 'image-validator.ts',
439
- 'svg-sanitizer.ts',
440
- 'shortcut-binding.service.ts',
441
- 'docx-parser.ts',
442
- 'docx-to-editor-html.ts',
443
- 'zip-reader.ts',
444
- 'inflate.ts',
445
- ],
372
+ files: ['rich-text-command-registry.service.ts', 'rich-text-editor.component.ts', 'rich-text-image-resizer.component.ts', 'rich-text-locales.ts', 'rich-text-markdown.service.ts', 'rich-text-mention.component.ts', 'rich-text-paste-normalizer.service.ts', 'rich-text-sanitizer.service.ts', 'rich-text-toolbar.component.ts'],
373
+ dependencies: ['autocomplete', 'button', 'dialog', 'emoji-picker', 'popover', 'scroll-area', 'separator'],
374
+ libFiles: ['docx-parser.ts', 'docx-to-editor-html.ts', 'image-validator.ts', 'inflate.ts', 'pdf-parser.ts', 'shortcut-binding.service.ts', 'svg-sanitizer.ts', 'zip-reader.ts'],
446
375
  shortcutDefinitions: [
447
376
  {
448
377
  exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
@@ -454,102 +383,54 @@ export const registry: Record<string, ComponentDefinition> = {
454
383
  // Chart Components
455
384
  'pie-chart': {
456
385
  name: 'pie-chart',
457
- files: [
458
- 'charts/pie-chart.component.ts',
459
- 'charts/chart.types.ts',
460
- 'charts/chart.utils.ts',
461
- ],
386
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/pie-chart.component.ts'],
462
387
  },
463
388
  'pie-chart-drilldown': {
464
389
  name: 'pie-chart-drilldown',
465
- files: [
466
- 'charts/pie-chart-drilldown.component.ts',
467
- 'charts/chart.types.ts',
468
- 'charts/chart.utils.ts',
469
- ],
390
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/pie-chart-drilldown.component.ts'],
470
391
  },
471
392
  'bar-chart': {
472
393
  name: 'bar-chart',
473
- files: [
474
- 'charts/bar-chart.component.ts',
475
- 'charts/chart.types.ts',
476
- 'charts/chart.utils.ts',
477
- ],
394
+ files: ['charts/bar-chart.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
478
395
  },
479
396
  'bar-chart-drilldown': {
480
397
  name: 'bar-chart-drilldown',
481
- files: [
482
- 'charts/bar-chart-drilldown.component.ts',
483
- 'charts/chart.types.ts',
484
- 'charts/chart.utils.ts',
485
- ],
398
+ files: ['charts/bar-chart-drilldown.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
486
399
  },
487
400
  'stacked-bar-chart': {
488
401
  name: 'stacked-bar-chart',
489
- files: [
490
- 'charts/stacked-bar-chart.component.ts',
491
- 'charts/chart.types.ts',
492
- 'charts/chart.utils.ts',
493
- ],
402
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/stacked-bar-chart.component.ts'],
494
403
  },
495
404
  'column-range-chart': {
496
405
  name: 'column-range-chart',
497
- files: [
498
- 'charts/column-range-chart.component.ts',
499
- 'charts/chart.types.ts',
500
- 'charts/chart.utils.ts',
501
- ],
406
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/column-range-chart.component.ts'],
502
407
  },
503
408
  'bar-race-chart': {
504
409
  name: 'bar-race-chart',
505
- files: [
506
- 'charts/bar-race-chart.component.ts',
507
- 'charts/chart.types.ts',
508
- 'charts/chart.utils.ts',
509
- ],
410
+ files: ['charts/bar-race-chart.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
510
411
  },
511
412
  'org-chart': {
512
413
  name: 'org-chart',
513
- files: [
514
- 'charts/org-chart.component.ts',
515
- 'charts/chart.types.ts',
516
- 'charts/chart.utils.ts',
517
- ],
414
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/org-chart.component.ts'],
518
415
  },
519
416
  'bento-grid': {
520
417
  name: 'bento-grid',
521
- dependencies: ['context-menu', 'component-outlet', 'icon'],
522
- files: [
523
- 'bento-grid.component.ts',
524
- ],
418
+ dependencies: ['component-outlet', 'context-menu'],
419
+ files: ['bento-grid.component.ts'],
525
420
  },
526
421
  'page-builder': {
527
422
  name: 'page-builder',
528
- dependencies: [
529
- 'bento-grid',
530
- 'button',
531
- 'input',
532
- 'label',
533
- 'select',
534
- 'switch',
535
- 'slider',
536
- 'icon'
537
- ],
538
- files: [
539
- 'page-builder/page-builder.component.ts',
540
- 'page-builder/page-builder.types.ts',
541
- 'page-builder/property-editor.component.ts',
542
- 'page-builder/page-renderer.component.ts'
543
- ],
423
+ dependencies: ['bento-grid', 'icon', 'select', 'switch'],
424
+ files: ['page-builder/page-builder.component.ts', 'page-builder/page-builder.types.ts', 'page-builder/property-editor.component.ts'],
544
425
  },
545
426
  'component-outlet': {
546
427
  name: 'component-outlet',
547
- files: ['component-outlet.directive.ts'],
428
+ files: ['component-outlet.directive.ts', 'data-table/component-pool.service.ts'],
548
429
  },
549
430
  'split-button': {
550
431
  name: 'split-button',
551
432
  files: ['split-button.component.ts'],
552
- dependencies: ['button', 'dropdown-menu'],
433
+ dependencies: ['button'],
553
434
  },
554
435
  // Animations
555
436
  'gradient-text': {
@@ -616,16 +497,26 @@ export const registry: Record<string, ComponentDefinition> = {
616
497
  name: 'particles',
617
498
  files: ['particles.component.ts'],
618
499
  },
619
- // Kanban
620
500
  kanban: {
621
501
  name: 'kanban',
622
- files: ['kanban.component.ts', 'kanban-locales.ts'],
502
+ files: ['kanban-locales.ts', 'kanban.component.ts'],
623
503
  libFiles: ['shortcut-binding.service.ts'],
624
- dependencies: [
625
- 'badge', 'avatar', 'scroll-area', 'separator',
626
- 'button', 'input', 'textarea', 'label',
627
- 'chip-list', 'autocomplete',
628
- 'dialog', 'alert-dialog', 'context-menu',
629
- ],
504
+ dependencies: ['alert-dialog', 'autocomplete', 'avatar', 'badge', 'button', 'chip-list', 'context-menu', 'dialog', 'input', 'label', 'scroll-area', 'separator', 'textarea'],
630
505
  },
631
- };
506
+ 'shortcut-bindings-dialog': {
507
+ name: 'shortcut-bindings-dialog',
508
+ files: ['shortcut-bindings-dialog.component.ts'],
509
+ libFiles: ['shortcut-binding.service.ts'],
510
+ dependencies: ['accordion', 'badge', 'button', 'dialog', 'scroll-area'],
511
+ },
512
+ });
513
+
514
+ export type ComponentName = keyof typeof registry;
515
+
516
+ export function isComponentName(name: string): name is ComponentName {
517
+ return name in registry;
518
+ }
519
+
520
+ export function getComponentNames(): ComponentName[] {
521
+ return Object.keys(registry) as ComponentName[];
522
+ }
@@ -2,8 +2,8 @@ import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
3
 
4
4
  export interface Config {
5
- $schema: string;
6
5
  style: 'default';
6
+ registry?: string;
7
7
  tailwind: {
8
8
  css: string;
9
9
  baseColor: 'neutral' | 'slate' | 'stone' | 'gray' | 'zinc';
@@ -19,7 +19,6 @@ export interface Config {
19
19
 
20
20
  export function getDefaultConfig(): Config {
21
21
  return {
22
- $schema: 'https://shadcn-angular.dev/schema.json',
23
22
  style: 'default',
24
23
  tailwind: {
25
24
  css: 'src/styles.scss',
@@ -35,6 +34,21 @@ export function getDefaultConfig(): Config {
35
34
  };
36
35
  }
37
36
 
37
+ function validateConfig(data: unknown): data is Config {
38
+ if (!data || typeof data !== 'object') return false;
39
+ const obj = data as Record<string, unknown>;
40
+
41
+ if (!obj['tailwind'] || typeof obj['tailwind'] !== 'object') return false;
42
+ const tw = obj['tailwind'] as Record<string, unknown>;
43
+ if (typeof tw['css'] !== 'string' || typeof tw['baseColor'] !== 'string') return false;
44
+
45
+ if (!obj['aliases'] || typeof obj['aliases'] !== 'object') return false;
46
+ const aliases = obj['aliases'] as Record<string, unknown>;
47
+ if (typeof aliases['components'] !== 'string' || typeof aliases['utils'] !== 'string' || typeof aliases['ui'] !== 'string') return false;
48
+
49
+ return true;
50
+ }
51
+
38
52
  export async function getConfig(cwd: string): Promise<Config | null> {
39
53
  const configPath = path.join(cwd, 'components.json');
40
54
 
@@ -43,7 +57,12 @@ export async function getConfig(cwd: string): Promise<Config | null> {
43
57
  }
44
58
 
45
59
  try {
46
- return await fs.readJson(configPath);
60
+ const data: unknown = await fs.readJson(configPath);
61
+ if (!validateConfig(data)) {
62
+ console.error('Error: components.json is missing required fields (tailwind.css, tailwind.baseColor, aliases.components, aliases.utils, aliases.ui).');
63
+ return null;
64
+ }
65
+ return data;
47
66
  } catch {
48
67
  return null;
49
68
  }
@@ -0,0 +1,52 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export function validateBranch(branch: string): void {
9
+ if (!/^[\w.\-/]+$/.test(branch)) {
10
+ throw new Error(`Invalid branch name: ${branch}`);
11
+ }
12
+ }
13
+
14
+ function getDefaultRegistryBaseUrl(branch: string): string {
15
+ validateBranch(branch);
16
+ return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components`;
17
+ }
18
+
19
+ export function getRegistryBaseUrl(branch: string, customRegistry?: string): string {
20
+ const base = customRegistry ?? getDefaultRegistryBaseUrl(branch);
21
+ return `${base}/ui`;
22
+ }
23
+
24
+ export function getLibRegistryBaseUrl(branch: string, customRegistry?: string): string {
25
+ const base = customRegistry ?? getDefaultRegistryBaseUrl(branch);
26
+ return `${base}/lib`;
27
+ }
28
+
29
+ export function getLocalComponentsDir(): string | null {
30
+ const localPath = path.resolve(__dirname, '../../../../components/ui');
31
+ return fs.existsSync(localPath) ? localPath : null;
32
+ }
33
+
34
+ export function getLocalLibDir(): string | null {
35
+ const localPath = path.resolve(__dirname, '../../../../components/lib');
36
+ return fs.existsSync(localPath) ? localPath : null;
37
+ }
38
+
39
+ export function resolveProjectPath(cwd: string, inputPath: string): string {
40
+ const resolved = path.resolve(cwd, inputPath);
41
+ const relative = path.relative(cwd, resolved);
42
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
43
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
44
+ }
45
+ return resolved;
46
+ }
47
+
48
+ export function aliasToProjectPath(aliasOrPath: string): string {
49
+ return aliasOrPath.startsWith('@/')
50
+ ? path.join('src', aliasOrPath.slice(2))
51
+ : aliasOrPath;
52
+ }