@gilav21/shadcn-angular 0.0.19 → 0.0.21

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,140 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { resolveDependencies, promptOptionalDependencies } from './add.js';
3
+ import { registry } from '../registry/index.js';
4
+ // ---------------------------------------------------------------------------
5
+ // resolveDependencies
6
+ // ---------------------------------------------------------------------------
7
+ describe('resolveDependencies', () => {
8
+ it('resolves a single component without dependencies', () => {
9
+ const result = resolveDependencies(['badge']);
10
+ expect(result).toContain('badge');
11
+ expect(result.size).toBe(1);
12
+ });
13
+ it('includes transitive dependencies', () => {
14
+ // button depends on ripple
15
+ const result = resolveDependencies(['button']);
16
+ expect(result).toContain('button');
17
+ expect(result).toContain('ripple');
18
+ });
19
+ it('resolves deep transitive chains', () => {
20
+ // date-picker -> calendar -> button -> ripple, calendar -> select
21
+ const result = resolveDependencies(['date-picker']);
22
+ expect(result).toContain('date-picker');
23
+ expect(result).toContain('calendar');
24
+ expect(result).toContain('button');
25
+ expect(result).toContain('ripple');
26
+ expect(result).toContain('select');
27
+ });
28
+ it('deduplicates shared dependencies across multiple inputs', () => {
29
+ // Both button-group and speed-dial depend on button
30
+ const result = resolveDependencies(['button-group', 'speed-dial']);
31
+ expect(result).toContain('button-group');
32
+ expect(result).toContain('speed-dial');
33
+ expect(result).toContain('button');
34
+ expect(result).toContain('ripple');
35
+ // Count how many times button appears — should be exactly once (it's a Set)
36
+ const asArray = [...result];
37
+ expect(asArray.filter((n) => n === 'button')).toHaveLength(1);
38
+ });
39
+ it('returns a Set containing all resolved names', () => {
40
+ const result = resolveDependencies(['badge']);
41
+ expect(result).toBeInstanceOf(Set);
42
+ });
43
+ it('handles components with no registry deps gracefully', () => {
44
+ const result = resolveDependencies(['separator']);
45
+ expect(result.size).toBe(1);
46
+ expect(result).toContain('separator');
47
+ });
48
+ });
49
+ // ---------------------------------------------------------------------------
50
+ // promptOptionalDependencies
51
+ // ---------------------------------------------------------------------------
52
+ vi.mock('prompts', () => ({
53
+ default: vi.fn(),
54
+ }));
55
+ describe('promptOptionalDependencies', () => {
56
+ it('returns empty array when no components have optional deps', async () => {
57
+ const resolved = new Set(['badge', 'button', 'ripple']);
58
+ const result = await promptOptionalDependencies(resolved, { yes: false, branch: 'master' });
59
+ expect(result).toEqual([]);
60
+ });
61
+ it('returns empty array with --yes flag (skip prompts)', async () => {
62
+ const resolved = new Set(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
63
+ const result = await promptOptionalDependencies(resolved, { yes: true, branch: 'master' });
64
+ expect(result).toEqual([]);
65
+ });
66
+ it('returns all optional dep names with --all flag', async () => {
67
+ const resolved = new Set(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
68
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
69
+ expect(result).toContain('context-menu');
70
+ });
71
+ it('filters out optional deps already in the resolved set', async () => {
72
+ // context-menu is already resolved, so it should not be offered
73
+ const resolved = new Set(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
74
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
75
+ expect(result).not.toContain('context-menu');
76
+ });
77
+ it('deduplicates optional deps across components', async () => {
78
+ // Both data-table and tree have context-menu as optional
79
+ const resolved = new Set(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
80
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
81
+ const contextMenuCount = result.filter((n) => n === 'context-menu').length;
82
+ expect(contextMenuCount).toBe(1);
83
+ });
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // Registry data integrity
87
+ // ---------------------------------------------------------------------------
88
+ describe('registry optional dependencies', () => {
89
+ it('data-table has context-menu as optional dependency', () => {
90
+ const dt = registry['data-table'];
91
+ expect(dt.optionalDependencies).toBeDefined();
92
+ const names = dt.optionalDependencies.map((d) => d.name);
93
+ expect(names).toContain('context-menu');
94
+ });
95
+ it('tree has context-menu as optional dependency', () => {
96
+ const tree = registry['tree'];
97
+ expect(tree.optionalDependencies).toBeDefined();
98
+ const names = tree.optionalDependencies.map((d) => d.name);
99
+ expect(names).toContain('context-menu');
100
+ });
101
+ it('every optional dependency name is a valid registry key', () => {
102
+ for (const [componentName, definition] of Object.entries(registry)) {
103
+ if (!definition.optionalDependencies)
104
+ continue;
105
+ for (const opt of definition.optionalDependencies) {
106
+ expect(registry[opt.name], `Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`).toBeDefined();
107
+ }
108
+ }
109
+ });
110
+ it('every dependency name is a valid registry key', () => {
111
+ for (const [componentName, definition] of Object.entries(registry)) {
112
+ if (!definition.dependencies)
113
+ continue;
114
+ for (const dep of definition.dependencies) {
115
+ expect(registry[dep], `Dependency "${dep}" in "${componentName}" is not a valid registry key`).toBeDefined();
116
+ }
117
+ }
118
+ });
119
+ });
120
+ // ---------------------------------------------------------------------------
121
+ // help command
122
+ // ---------------------------------------------------------------------------
123
+ describe('help command', () => {
124
+ it('prints commands, optional dependencies, and component categories', async () => {
125
+ const { help } = await import('./help.js');
126
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
127
+ help();
128
+ expect(spy).toHaveBeenCalledTimes(1);
129
+ const output = spy.mock.calls[0][0];
130
+ expect(output).toContain('Commands');
131
+ expect(output).toContain('init');
132
+ expect(output).toContain('add');
133
+ expect(output).toContain('Optional Dependencies');
134
+ expect(output).toContain('Available Components');
135
+ expect(output).toContain('UI');
136
+ expect(output).toContain('Charts');
137
+ expect(output).toContain('Animation');
138
+ spy.mockRestore();
139
+ });
140
+ });
@@ -0,0 +1 @@
1
+ export declare function help(): void;
@@ -0,0 +1,108 @@
1
+ import chalk from 'chalk';
2
+ import { registry } from '../registry/index.js';
3
+ const CATEGORY_ORDER = ['UI', 'Charts', 'Layout / Page Building', 'Animation', 'Kanban'];
4
+ const ANIMATION_COMPONENTS = new Set([
5
+ 'gradient-text', 'flip-text', 'meteors', 'shine-border', 'scroll-progress',
6
+ 'blur-fade', 'ripple', 'marquee', 'word-rotate', 'morphing-text',
7
+ 'typing-animation', 'wobble-card', 'magnetic', 'orbit', 'stagger-children',
8
+ 'particles', 'confetti', 'number-ticker', 'text-reveal', 'streaming-text', 'sparkles',
9
+ ]);
10
+ const KANBAN_COMPONENTS = new Set(['kanban']);
11
+ const LAYOUT_COMPONENTS = new Set(['bento-grid', 'page-builder']);
12
+ function categorize(name) {
13
+ const def = registry[name];
14
+ if (!def)
15
+ return 'UI';
16
+ const hasChartFile = def.files.some(f => f.startsWith('charts/'));
17
+ if (hasChartFile)
18
+ return 'Charts';
19
+ if (LAYOUT_COMPONENTS.has(name))
20
+ return 'Layout / Page Building';
21
+ if (ANIMATION_COMPONENTS.has(name))
22
+ return 'Animation';
23
+ if (KANBAN_COMPONENTS.has(name))
24
+ return 'Kanban';
25
+ return 'UI';
26
+ }
27
+ function buildComponentsByCategory() {
28
+ const groups = new Map();
29
+ for (const cat of CATEGORY_ORDER) {
30
+ groups.set(cat, []);
31
+ }
32
+ for (const name of Object.keys(registry)) {
33
+ const cat = categorize(name);
34
+ const list = groups.get(cat);
35
+ if (list)
36
+ list.push(name);
37
+ }
38
+ for (const list of groups.values()) {
39
+ list.sort();
40
+ }
41
+ return groups;
42
+ }
43
+ function formatComponentList(names, columns = 4) {
44
+ const colWidth = 26;
45
+ const lines = [];
46
+ for (let i = 0; i < names.length; i += columns) {
47
+ const row = names.slice(i, i + columns);
48
+ lines.push(' ' + row.map(n => n.padEnd(colWidth)).join(''));
49
+ }
50
+ return lines.join('\n');
51
+ }
52
+ function buildCommandsSection() {
53
+ const branchDefault = chalk.gray('(default: master)');
54
+ return [
55
+ chalk.bold('Commands'),
56
+ '',
57
+ ' ' + chalk.cyan('init') + ' Initialize shadcn-angular in your project',
58
+ ' ' + chalk.gray('-y, --yes') + ' Skip confirmation prompt',
59
+ ' ' + chalk.gray('-d, --defaults') + ' Use default configuration',
60
+ ' ' + chalk.gray('-b, --branch') + ' <branch> GitHub branch to fetch from ' + branchDefault,
61
+ '',
62
+ ' ' + chalk.cyan('add') + ' Add component(s) to your project',
63
+ ' ' + chalk.gray('[components...]') + ' One or more component names',
64
+ ' ' + chalk.gray('-y, --yes') + ' Skip confirmation prompt',
65
+ ' ' + chalk.gray('-o, --overwrite') + ' Overwrite existing files',
66
+ ' ' + chalk.gray('-a, --all') + ' Add all available components',
67
+ ' ' + chalk.gray('-p, --path') + ' <path> Custom install path',
68
+ ' ' + chalk.gray('--remote') + ' Force remote fetch from GitHub registry',
69
+ ' ' + chalk.gray('-b, --branch') + ' <branch> GitHub branch to fetch from ' + branchDefault,
70
+ '',
71
+ ' ' + chalk.cyan('help') + ' Show this reference',
72
+ '',
73
+ ];
74
+ }
75
+ function buildOptionalDepsSection() {
76
+ return [
77
+ chalk.bold('Optional Dependencies'),
78
+ '',
79
+ ' Some components offer companion packages during installation.',
80
+ ' With ' + chalk.cyan('--yes') + ' they are skipped; with ' + chalk.cyan('--all') + ' they are included automatically.',
81
+ '',
82
+ ];
83
+ }
84
+ function buildComponentsSection() {
85
+ const groups = buildComponentsByCategory();
86
+ const lines = [
87
+ chalk.bold('Available Components'),
88
+ '',
89
+ ];
90
+ for (const [category, names] of groups) {
91
+ if (names.length === 0)
92
+ continue;
93
+ const countLabel = chalk.gray('(' + String(names.length) + ')');
94
+ lines.push(' ' + chalk.yellow(category) + ' ' + countLabel, formatComponentList(names), '');
95
+ }
96
+ return lines;
97
+ }
98
+ export function help() {
99
+ const output = [
100
+ '',
101
+ chalk.bold.underline('shadcn-angular CLI'),
102
+ '',
103
+ ...buildCommandsSection(),
104
+ ...buildOptionalDepsSection(),
105
+ ...buildComponentsSection(),
106
+ ];
107
+ console.log(output.join('\n'));
108
+ }
@@ -1,6 +1,7 @@
1
1
  interface InitOptions {
2
2
  yes?: boolean;
3
3
  defaults?: boolean;
4
+ branch: string;
4
5
  }
5
6
  export declare function init(options: InitOptions): Promise<void>;
6
7
  export {};
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs-extra';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
4
  import prompts from 'prompts';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
@@ -9,7 +9,9 @@ import { getStylesTemplate } from '../templates/styles.js';
9
9
  import { getUtilsTemplate } from '../templates/utils.js';
10
10
  import { installPackages } from '../utils/package-manager.js';
11
11
  import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
12
- const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
12
+ function getLibRegistryBaseUrl(branch) {
13
+ return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/lib`;
14
+ }
13
15
  const __filename = fileURLToPath(import.meta.url);
14
16
  const __dirname = path.dirname(__filename);
15
17
  function getLocalLibDir() {
@@ -19,7 +21,7 @@ function getLocalLibDir() {
19
21
  }
20
22
  return null;
21
23
  }
22
- async function fetchLibFileContent(file) {
24
+ async function fetchLibFileContent(file, branch) {
23
25
  const localLibDir = getLocalLibDir();
24
26
  if (localLibDir) {
25
27
  const localPath = path.join(localLibDir, file);
@@ -27,7 +29,7 @@ async function fetchLibFileContent(file) {
27
29
  return fs.readFile(localPath, 'utf-8');
28
30
  }
29
31
  }
30
- const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
32
+ const url = `${getLibRegistryBaseUrl(branch)}/${file}`;
31
33
  const response = await fetch(url);
32
34
  if (!response.ok) {
33
35
  throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
@@ -78,7 +80,6 @@ export async function init(options) {
78
80
  let createShortcutRegistry = true;
79
81
  if (options.defaults || options.yes) {
80
82
  config = getDefaultConfig();
81
- createShortcutRegistry = true;
82
83
  }
83
84
  else {
84
85
  const THEME_COLORS = {
@@ -200,7 +201,7 @@ export async function init(options) {
200
201
  await fs.writeFile(path.join(libDir, 'utils.ts'), getUtilsTemplate());
201
202
  spinner.text = 'Created utils.ts';
202
203
  const shortcutServicePath = path.join(libDir, 'shortcut-binding.service.ts');
203
- const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts');
204
+ const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts', options.branch);
204
205
  await fs.writeFile(shortcutServicePath, shortcutServiceContent);
205
206
  spinner.text = 'Created shortcut-binding.service.ts';
206
207
  if (createShortcutRegistry) {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander';
3
3
  import { init } from './commands/init.js';
4
4
  import { add } from './commands/add.js';
5
+ import { help } from './commands/help.js';
5
6
  const program = new Command();
6
7
  program
7
8
  .name('shadcn-angular')
@@ -12,6 +13,7 @@ program
12
13
  .description('Initialize shadcn-angular in your project')
13
14
  .option('-y, --yes', 'Skip confirmation prompt')
14
15
  .option('-d, --defaults', 'Use default configuration')
16
+ .option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
15
17
  .action(init);
16
18
  program
17
19
  .command('add')
@@ -22,5 +24,10 @@ program
22
24
  .option('-a, --all', 'Add all available components')
23
25
  .option('-p, --path <path>', 'The path to add the component to')
24
26
  .option('--remote', 'Force remote fetch from GitHub registry')
27
+ .option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
25
28
  .action(add);
29
+ program
30
+ .command('help')
31
+ .description('Show detailed usage information')
32
+ .action(help);
26
33
  program.parse();
@@ -1,7 +1,13 @@
1
+ export interface OptionalDependency {
2
+ readonly name: string;
3
+ readonly description: string;
4
+ }
1
5
  export interface ComponentDefinition {
2
6
  name: string;
3
7
  files: string[];
8
+ peerFiles?: string[];
4
9
  dependencies?: string[];
10
+ optionalDependencies?: readonly OptionalDependency[];
5
11
  npmDependencies?: string[];
6
12
  libFiles?: string[];
7
13
  shortcutDefinitions?: {
@@ -127,8 +127,12 @@ export const registry = {
127
127
  'data-table/data-table-column-header.component.ts',
128
128
  'data-table/data-table-pagination.component.ts',
129
129
  'data-table/data-table.types.ts',
130
+ 'data-table/data-table.utils.ts',
130
131
  'data-table/index.ts',
131
132
  ],
133
+ peerFiles: [
134
+ 'context-menu-integrations.ts',
135
+ ],
132
136
  dependencies: [
133
137
  'table',
134
138
  'input',
@@ -141,6 +145,9 @@ export const registry = {
141
145
  'icon',
142
146
  ],
143
147
  libFiles: ['xlsx.ts'],
148
+ optionalDependencies: [
149
+ { name: 'context-menu', description: 'Enables right-click context menus on rows and headers' },
150
+ ],
144
151
  },
145
152
  dialog: {
146
153
  name: 'dialog',
@@ -337,6 +344,9 @@ export const registry = {
337
344
  name: 'tree',
338
345
  files: ['tree.component.ts'],
339
346
  dependencies: ['icon'],
347
+ optionalDependencies: [
348
+ { name: 'context-menu', description: 'Enables right-click context menus on tree nodes' },
349
+ ],
340
350
  },
341
351
  'speed-dial': {
342
352
  name: 'speed-dial',
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs-extra';
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  export function getDefaultConfig() {
4
4
  return {
5
5
  $schema: 'https://shadcn-angular.dev/schema.json',
@@ -1,6 +1,6 @@
1
1
  import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
- import path from 'path';
3
+ import path from 'node:path';
4
4
  export async function getPackageManager(cwd) {
5
5
  const userAgent = process.env['npm_config_user_agent'];
6
6
  if (userAgent) {
@@ -29,12 +29,7 @@ export async function installPackages(packages, options) {
29
29
  if (options.dev)
30
30
  args.push('-D');
31
31
  }
32
- else if (packageManager === 'yarn') {
33
- args.push('add');
34
- if (options.dev)
35
- args.push('-D');
36
- }
37
- else if (packageManager === 'pnpm') {
32
+ else if (packageManager === 'yarn' || packageManager === 'pnpm') {
38
33
  args.push('add');
39
34
  if (options.dev)
40
35
  args.push('-D');
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs-extra';
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  function aliasToProjectPath(aliasOrPath) {
4
4
  return aliasOrPath.startsWith('@/')
5
5
  ? path.join('src', aliasOrPath.slice(2))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"
@@ -0,0 +1,170 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { resolveDependencies, promptOptionalDependencies } from './add.js';
3
+ import { registry, type ComponentName, type ComponentDefinition } from '../registry/index.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // resolveDependencies
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('resolveDependencies', () => {
10
+ it('resolves a single component without dependencies', () => {
11
+ const result = resolveDependencies(['badge']);
12
+ expect(result).toContain('badge');
13
+ expect(result.size).toBe(1);
14
+ });
15
+
16
+ it('includes transitive dependencies', () => {
17
+ // button depends on ripple
18
+ const result = resolveDependencies(['button']);
19
+ expect(result).toContain('button');
20
+ expect(result).toContain('ripple');
21
+ });
22
+
23
+ it('resolves deep transitive chains', () => {
24
+ // date-picker -> calendar -> button -> ripple, calendar -> select
25
+ const result = resolveDependencies(['date-picker']);
26
+ expect(result).toContain('date-picker');
27
+ expect(result).toContain('calendar');
28
+ expect(result).toContain('button');
29
+ expect(result).toContain('ripple');
30
+ expect(result).toContain('select');
31
+ });
32
+
33
+ it('deduplicates shared dependencies across multiple inputs', () => {
34
+ // Both button-group and speed-dial depend on button
35
+ const result = resolveDependencies(['button-group', 'speed-dial']);
36
+ expect(result).toContain('button-group');
37
+ expect(result).toContain('speed-dial');
38
+ expect(result).toContain('button');
39
+ expect(result).toContain('ripple');
40
+
41
+ // Count how many times button appears — should be exactly once (it's a Set)
42
+ const asArray = [...result];
43
+ expect(asArray.filter((n: string) => n === 'button')).toHaveLength(1);
44
+ });
45
+
46
+ it('returns a Set containing all resolved names', () => {
47
+ const result = resolveDependencies(['badge']);
48
+ expect(result).toBeInstanceOf(Set);
49
+ });
50
+
51
+ it('handles components with no registry deps gracefully', () => {
52
+ const result = resolveDependencies(['separator']);
53
+ expect(result.size).toBe(1);
54
+ expect(result).toContain('separator');
55
+ });
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // promptOptionalDependencies
60
+ // ---------------------------------------------------------------------------
61
+
62
+ vi.mock('prompts', () => ({
63
+ default: vi.fn(),
64
+ }));
65
+
66
+ describe('promptOptionalDependencies', () => {
67
+ it('returns empty array when no components have optional deps', async () => {
68
+ const resolved = new Set<ComponentName>(['badge', 'button', 'ripple']);
69
+ const result = await promptOptionalDependencies(resolved, { yes: false, branch: 'master' });
70
+ expect(result).toEqual([]);
71
+ });
72
+
73
+ it('returns empty array with --yes flag (skip prompts)', async () => {
74
+ const resolved = new Set<ComponentName>(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
75
+ const result = await promptOptionalDependencies(resolved, { yes: true, branch: 'master' });
76
+ expect(result).toEqual([]);
77
+ });
78
+
79
+ it('returns all optional dep names with --all flag', async () => {
80
+ const resolved = new Set<ComponentName>(['data-table', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
81
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
82
+ expect(result).toContain('context-menu');
83
+ });
84
+
85
+ it('filters out optional deps already in the resolved set', async () => {
86
+ // context-menu is already resolved, so it should not be offered
87
+ const resolved = new Set<ComponentName>(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
88
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
89
+ expect(result).not.toContain('context-menu');
90
+ });
91
+
92
+ it('deduplicates optional deps across components', async () => {
93
+ // Both data-table and tree have context-menu as optional
94
+ const resolved = new Set<ComponentName>(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
95
+ const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
96
+ const contextMenuCount = result.filter((n: string) => n === 'context-menu').length;
97
+ expect(contextMenuCount).toBe(1);
98
+ });
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Registry data integrity
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe('registry optional dependencies', () => {
106
+ it('data-table has context-menu as optional dependency', () => {
107
+ const dt = registry['data-table'];
108
+ expect(dt.optionalDependencies).toBeDefined();
109
+ const names = dt.optionalDependencies!.map((d: { name: string }) => d.name);
110
+ expect(names).toContain('context-menu');
111
+ });
112
+
113
+ it('tree has context-menu as optional dependency', () => {
114
+ const tree = registry['tree'];
115
+ expect(tree.optionalDependencies).toBeDefined();
116
+ const names = tree.optionalDependencies!.map((d: { name: string }) => d.name);
117
+ expect(names).toContain('context-menu');
118
+ });
119
+
120
+ it('every optional dependency name is a valid registry key', () => {
121
+ for (const [componentName, definition] of Object.entries(registry) as [string, ComponentDefinition][]) {
122
+ if (!definition.optionalDependencies) continue;
123
+ for (const opt of definition.optionalDependencies) {
124
+ expect(
125
+ registry[opt.name],
126
+ `Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`,
127
+ ).toBeDefined();
128
+ }
129
+ }
130
+ });
131
+
132
+ it('every dependency name is a valid registry key', () => {
133
+ for (const [componentName, definition] of Object.entries(registry) as [string, ComponentDefinition][]) {
134
+ if (!definition.dependencies) continue;
135
+ for (const dep of definition.dependencies) {
136
+ expect(
137
+ registry[dep],
138
+ `Dependency "${dep}" in "${componentName}" is not a valid registry key`,
139
+ ).toBeDefined();
140
+ }
141
+ }
142
+ });
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // help command
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe('help command', () => {
150
+ it('prints commands, optional dependencies, and component categories', async () => {
151
+ const { help } = await import('./help.js');
152
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
153
+
154
+ help();
155
+
156
+ expect(spy).toHaveBeenCalledTimes(1);
157
+ const output = spy.mock.calls[0][0] as string;
158
+
159
+ expect(output).toContain('Commands');
160
+ expect(output).toContain('init');
161
+ expect(output).toContain('add');
162
+ expect(output).toContain('Optional Dependencies');
163
+ expect(output).toContain('Available Components');
164
+ expect(output).toContain('UI');
165
+ expect(output).toContain('Charts');
166
+ expect(output).toContain('Animation');
167
+
168
+ spy.mockRestore();
169
+ });
170
+ });