@gilav21/shadcn-angular 0.0.20 → 0.0.22

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,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';
@@ -80,7 +80,6 @@ export async function init(options) {
80
80
  let createShortcutRegistry = true;
81
81
  if (options.defaults || options.yes) {
82
82
  config = getDefaultConfig();
83
- createShortcutRegistry = true;
84
83
  }
85
84
  else {
86
85
  const THEME_COLORS = {
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')
@@ -25,4 +26,8 @@ program
25
26
  .option('--remote', 'Force remote fetch from GitHub registry')
26
27
  .option('-b, --branch <branch>', 'GitHub branch to fetch components from', 'master')
27
28
  .action(add);
29
+ program
30
+ .command('help')
31
+ .description('Show detailed usage information')
32
+ .action(help);
28
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?: {
@@ -126,9 +126,14 @@ export const registry = {
126
126
  'data-table/data-table.component.ts',
127
127
  'data-table/data-table-column-header.component.ts',
128
128
  'data-table/data-table-pagination.component.ts',
129
+ 'data-table/data-table-multiselect-filter.component.ts',
129
130
  'data-table/data-table.types.ts',
131
+ 'data-table/data-table.utils.ts',
130
132
  'data-table/index.ts',
131
133
  ],
134
+ peerFiles: [
135
+ 'context-menu-integrations.ts',
136
+ ],
132
137
  dependencies: [
133
138
  'table',
134
139
  'input',
@@ -139,8 +144,13 @@ export const registry = {
139
144
  'popover',
140
145
  'component-outlet',
141
146
  'icon',
147
+ 'command',
148
+ 'badge',
142
149
  ],
143
150
  libFiles: ['xlsx.ts'],
151
+ optionalDependencies: [
152
+ { name: 'context-menu', description: 'Enables right-click context menus on rows and headers' },
153
+ ],
144
154
  },
145
155
  dialog: {
146
156
  name: 'dialog',
@@ -337,6 +347,9 @@ export const registry = {
337
347
  name: 'tree',
338
348
  files: ['tree.component.ts'],
339
349
  dependencies: ['icon'],
350
+ optionalDependencies: [
351
+ { name: 'context-menu', description: 'Enables right-click context menus on tree nodes' },
352
+ ],
340
353
  },
341
354
  'speed-dial': {
342
355
  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.20",
3
+ "version": "0.0.22",
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
+ });