@gilav21/shadcn-angular 0.0.24 → 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',
@@ -103,6 +102,7 @@ export const registry: Record<string, ComponentDefinition> = {
103
102
  name: 'command',
104
103
  files: ['command.component.ts'],
105
104
  dependencies: ['dialog'],
105
+ libFiles: ['shortcut-binding.service.ts'],
106
106
  shortcutDefinitions: [
107
107
  {
108
108
  exportName: 'COMMAND_DIALOG_SHORTCUT_DEFINITIONS',
@@ -113,7 +113,7 @@ export const registry: Record<string, ComponentDefinition> = {
113
113
  },
114
114
  'context-menu': {
115
115
  name: 'context-menu',
116
- files: ['context-menu.component.ts', 'context-menu-integrations.ts'],
116
+ files: ['context-menu.component.ts'],
117
117
  },
118
118
  'date-picker': {
119
119
  name: 'date-picker',
@@ -123,7 +123,7 @@ export const registry: Record<string, ComponentDefinition> = {
123
123
  chat: {
124
124
  name: 'chat',
125
125
  files: ['chat.component.ts'],
126
- dependencies: ['avatar', 'button', 'textarea', 'scroll-area'],
126
+ dependencies: ['avatar', 'button', 'scroll-area', 'textarea'],
127
127
  },
128
128
  'streaming-text': {
129
129
  name: 'streaming-text',
@@ -137,7 +137,7 @@ export const registry: Record<string, ComponentDefinition> = {
137
137
  'code-block': {
138
138
  name: 'code-block',
139
139
  files: ['code-block.component.ts'],
140
- dependencies: ['button', 'scroll-area'],
140
+ dependencies: ['button'],
141
141
  },
142
142
  'text-reveal': {
143
143
  name: 'text-reveal',
@@ -145,31 +145,11 @@ export const registry: Record<string, ComponentDefinition> = {
145
145
  },
146
146
  'data-table': {
147
147
  name: 'data-table',
148
- files: [
149
- 'data-table/data-table.component.ts',
150
- 'data-table/data-table-column-header.component.ts',
151
- 'data-table/data-table-pagination.component.ts',
152
- 'data-table/data-table-multiselect-filter.component.ts',
153
- 'data-table/data-table.types.ts',
154
- 'data-table/data-table.utils.ts',
155
- 'data-table/index.ts',
156
- ],
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'],
157
149
  peerFiles: [
158
150
  'context-menu-integrations.ts',
159
151
  ],
160
- dependencies: [
161
- 'table',
162
- 'input',
163
- 'button',
164
- 'checkbox',
165
- 'select',
166
- 'pagination',
167
- 'popover',
168
- 'component-outlet',
169
- 'icon',
170
- 'command',
171
- 'badge',
172
- ],
152
+ dependencies: ['badge', 'button', 'calendar', 'checkbox', 'command', 'component-outlet', 'context-menu', 'icon', 'input', 'pagination', 'popover', 'select', 'table'],
173
153
  libFiles: ['xlsx.ts'],
174
154
  optionalDependencies: [
175
155
  { name: 'context-menu', description: 'Enables right-click context menus on rows and headers' },
@@ -181,18 +161,12 @@ export const registry: Record<string, ComponentDefinition> = {
181
161
  },
182
162
  dock: {
183
163
  name: 'dock',
184
- files: [
185
- 'dock.component.ts',
186
- 'dock-item.component.ts',
187
- 'dock-icon.component.ts',
188
- 'dock-label.component.ts',
189
- ],
190
- dependencies: ['icon'],
164
+ files: ['dock-icon.component.ts', 'dock-item.component.ts', 'dock-label.component.ts', 'dock.component.ts'],
191
165
  },
192
166
  'tree-select': {
193
167
  name: 'tree-select',
194
168
  files: ['tree-select.component.ts'],
195
- dependencies: ['popover', 'tree', 'icon'],
169
+ dependencies: ['popover', 'tree'],
196
170
  },
197
171
  'virtual-scroll': {
198
172
  name: 'virtual-scroll',
@@ -221,7 +195,7 @@ export const registry: Record<string, ComponentDefinition> = {
221
195
  },
222
196
  icon: {
223
197
  name: 'icon',
224
- files: ['icon.component.ts'],
198
+ files: ['icon.component.ts', 'icon.token.ts'],
225
199
  },
226
200
 
227
201
  'file-upload': {
@@ -233,17 +207,7 @@ export const registry: Record<string, ComponentDefinition> = {
233
207
  name: 'file-viewer',
234
208
  files: ['file-viewer.component.ts'],
235
209
  dependencies: ['spinner'],
236
- libFiles: [
237
- 'file-type-detector.ts',
238
- 'inflate.ts',
239
- 'zip-reader.ts',
240
- 'pptx-parser.ts',
241
- 'xlsx-reader.ts',
242
- 'docx-parser.ts',
243
- 'doc-enhanced-parser.ts',
244
- 'ppt-parser.ts',
245
- 'svg-sanitizer.ts',
246
- ],
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'],
247
211
  },
248
212
  'hover-card': {
249
213
  name: 'hover-card',
@@ -251,12 +215,11 @@ export const registry: Record<string, ComponentDefinition> = {
251
215
  },
252
216
  input: {
253
217
  name: 'input',
254
- files: ['input.component.ts', 'input-group.token.ts'],
218
+ files: ['input-group.token.ts', 'input.component.ts'],
255
219
  },
256
220
  'input-group': {
257
221
  name: 'input-group',
258
222
  files: ['input-group.component.ts', 'input-group.token.ts'],
259
- dependencies: ['input'],
260
223
  },
261
224
  'input-otp': {
262
225
  name: 'input-otp',
@@ -329,7 +292,7 @@ export const registry: Record<string, ComponentDefinition> = {
329
292
  sidebar: {
330
293
  name: 'sidebar',
331
294
  files: ['sidebar.component.ts'],
332
- dependencies: ['scroll-area', 'tooltip', 'icon'],
295
+ dependencies: ['scroll-area', 'tooltip'],
333
296
  },
334
297
  skeleton: {
335
298
  name: 'skeleton',
@@ -361,7 +324,7 @@ export const registry: Record<string, ComponentDefinition> = {
361
324
  },
362
325
  textarea: {
363
326
  name: 'textarea',
364
- files: ['textarea.component.ts', 'input-group.token.ts'],
327
+ files: ['input-group.token.ts', 'textarea.component.ts'],
365
328
  },
366
329
  timeline: {
367
330
  name: 'timeline',
@@ -386,51 +349,29 @@ export const registry: Record<string, ComponentDefinition> = {
386
349
  tree: {
387
350
  name: 'tree',
388
351
  files: ['tree.component.ts'],
389
- dependencies: ['icon'],
390
352
  optionalDependencies: [
391
353
  { name: 'context-menu', description: 'Enables right-click context menus on tree nodes' },
392
354
  ],
393
355
  },
394
356
  'speed-dial': {
395
357
  name: 'speed-dial',
396
- files: ['speed-dial.component.ts'],
397
- dependencies: ['button']
358
+ files: ['speed-dial.component.ts']
398
359
  },
399
360
  'chip-list': {
400
361
  name: 'chip-list',
401
- files: ['chip-list.component.ts'],
402
- dependencies: ['badge', 'button', 'input', 'input-group'],
362
+ files: ['chip-list.component.ts', 'input-group.token.ts'],
363
+ dependencies: ['badge', 'button', 'input'],
403
364
  },
404
365
  'emoji-picker': {
405
366
  name: 'emoji-picker',
406
- files: ['emoji-picker.component.ts', 'emoji-data.ts'],
367
+ files: ['emoji-data.ts', 'emoji-picker.component.ts'],
407
368
  dependencies: ['input', 'scroll-area', 'tooltip'],
408
369
  },
409
370
  'rich-text-editor': {
410
371
  name: 'rich-text-editor',
411
- files: [
412
- 'rich-text-editor.component.ts',
413
- 'rich-text-toolbar.component.ts',
414
- 'rich-text-sanitizer.service.ts',
415
- 'rich-text-markdown.service.ts',
416
- 'rich-text-paste-normalizer.service.ts',
417
- 'rich-text-command-registry.service.ts',
418
- 'rich-text-mention.component.ts',
419
- 'rich-text-image-resizer.component.ts',
420
- 'rich-text-locales.ts',
421
- ],
422
- dependencies: [
423
- 'button',
424
- 'separator',
425
- 'popover',
426
- 'emoji-picker',
427
- 'autocomplete',
428
- 'select',
429
- 'input',
430
- 'dialog',
431
- 'scroll-area',
432
- ],
433
- libFiles: ['pdf-parser.ts', 'image-validator.ts', 'svg-sanitizer.ts'],
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'],
434
375
  shortcutDefinitions: [
435
376
  {
436
377
  exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
@@ -442,102 +383,54 @@ export const registry: Record<string, ComponentDefinition> = {
442
383
  // Chart Components
443
384
  'pie-chart': {
444
385
  name: 'pie-chart',
445
- files: [
446
- 'charts/pie-chart.component.ts',
447
- 'charts/chart.types.ts',
448
- 'charts/chart.utils.ts',
449
- ],
386
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/pie-chart.component.ts'],
450
387
  },
451
388
  'pie-chart-drilldown': {
452
389
  name: 'pie-chart-drilldown',
453
- files: [
454
- 'charts/pie-chart-drilldown.component.ts',
455
- 'charts/chart.types.ts',
456
- 'charts/chart.utils.ts',
457
- ],
390
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/pie-chart-drilldown.component.ts'],
458
391
  },
459
392
  'bar-chart': {
460
393
  name: 'bar-chart',
461
- files: [
462
- 'charts/bar-chart.component.ts',
463
- 'charts/chart.types.ts',
464
- 'charts/chart.utils.ts',
465
- ],
394
+ files: ['charts/bar-chart.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
466
395
  },
467
396
  'bar-chart-drilldown': {
468
397
  name: 'bar-chart-drilldown',
469
- files: [
470
- 'charts/bar-chart-drilldown.component.ts',
471
- 'charts/chart.types.ts',
472
- 'charts/chart.utils.ts',
473
- ],
398
+ files: ['charts/bar-chart-drilldown.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
474
399
  },
475
400
  'stacked-bar-chart': {
476
401
  name: 'stacked-bar-chart',
477
- files: [
478
- 'charts/stacked-bar-chart.component.ts',
479
- 'charts/chart.types.ts',
480
- 'charts/chart.utils.ts',
481
- ],
402
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/stacked-bar-chart.component.ts'],
482
403
  },
483
404
  'column-range-chart': {
484
405
  name: 'column-range-chart',
485
- files: [
486
- 'charts/column-range-chart.component.ts',
487
- 'charts/chart.types.ts',
488
- 'charts/chart.utils.ts',
489
- ],
406
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/column-range-chart.component.ts'],
490
407
  },
491
408
  'bar-race-chart': {
492
409
  name: 'bar-race-chart',
493
- files: [
494
- 'charts/bar-race-chart.component.ts',
495
- 'charts/chart.types.ts',
496
- 'charts/chart.utils.ts',
497
- ],
410
+ files: ['charts/bar-race-chart.component.ts', 'charts/chart.types.ts', 'charts/chart.utils.ts'],
498
411
  },
499
412
  'org-chart': {
500
413
  name: 'org-chart',
501
- files: [
502
- 'charts/org-chart.component.ts',
503
- 'charts/chart.types.ts',
504
- 'charts/chart.utils.ts',
505
- ],
414
+ files: ['charts/chart.types.ts', 'charts/chart.utils.ts', 'charts/org-chart.component.ts'],
506
415
  },
507
416
  'bento-grid': {
508
417
  name: 'bento-grid',
509
- dependencies: ['context-menu', 'component-outlet', 'icon'],
510
- files: [
511
- 'bento-grid.component.ts',
512
- ],
418
+ dependencies: ['component-outlet', 'context-menu'],
419
+ files: ['bento-grid.component.ts'],
513
420
  },
514
421
  'page-builder': {
515
422
  name: 'page-builder',
516
- dependencies: [
517
- 'bento-grid',
518
- 'button',
519
- 'input',
520
- 'label',
521
- 'select',
522
- 'switch',
523
- 'slider',
524
- 'icon'
525
- ],
526
- files: [
527
- 'page-builder/page-builder.component.ts',
528
- 'page-builder/page-builder.types.ts',
529
- 'page-builder/property-editor.component.ts',
530
- 'page-builder/page-renderer.component.ts'
531
- ],
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'],
532
425
  },
533
426
  'component-outlet': {
534
427
  name: 'component-outlet',
535
- files: ['component-outlet.directive.ts'],
428
+ files: ['component-outlet.directive.ts', 'data-table/component-pool.service.ts'],
536
429
  },
537
430
  'split-button': {
538
431
  name: 'split-button',
539
432
  files: ['split-button.component.ts'],
540
- dependencies: ['button', 'dropdown-menu'],
433
+ dependencies: ['button'],
541
434
  },
542
435
  // Animations
543
436
  'gradient-text': {
@@ -604,15 +497,26 @@ export const registry: Record<string, ComponentDefinition> = {
604
497
  name: 'particles',
605
498
  files: ['particles.component.ts'],
606
499
  },
607
- // Kanban
608
500
  kanban: {
609
501
  name: 'kanban',
610
- files: ['kanban.component.ts', 'kanban-locales.ts'],
611
- dependencies: [
612
- 'badge', 'avatar', 'scroll-area', 'separator',
613
- 'button', 'input', 'textarea', 'label',
614
- 'chip-list', 'autocomplete',
615
- 'dialog', 'alert-dialog', 'context-menu',
616
- ],
502
+ files: ['kanban-locales.ts', 'kanban.component.ts'],
503
+ libFiles: ['shortcut-binding.service.ts'],
504
+ dependencies: ['alert-dialog', 'autocomplete', 'avatar', 'badge', 'button', 'chip-list', 'context-menu', 'dialog', 'input', 'label', 'scroll-area', 'separator', 'textarea'],
617
505
  },
618
- };
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
+ }