Skip to content

Commit ef8dc26

Browse files
committed
fix(cli): handle pnpm build approvals
Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 686ff96 commit ef8dc26

14 files changed

Lines changed: 550 additions & 25 deletions

File tree

‎projects/cli/README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ Install to Cursor with the MCP configuration below.
115115
}
116116
```
117117

118+
### Codex
119+
120+
Install to Codex with the MCP configuration below.
121+
122+
```toml
123+
[mcp_servers.elements]
124+
description = "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples"
125+
command = "nve"
126+
args = ["mcp"]
127+
```
128+
118129
### Prompts
119130

120131
| Prompt | Description | Example Prompt |

‎projects/internals/tools/src/api/utils.test.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import {
88
getContextAPIs,
99
getContextTokens,
1010
getPublishedPackageNames,
11+
searchContextAPIs,
1112
type PartialAPIResult
1213
} from './utils.js';
1314

15+
vi.mock('@internals/metadata', () => ({
16+
ApiService: { search: vi.fn() }
17+
}));
18+
1419
describe('getPublishedPackageNames', () => {
1520
const projects = [
1621
{
@@ -613,4 +618,88 @@ describe('attributeMetadataToMarkdown', () => {
613618

614619
expect(markdown.includes('| `disabled` | `string` |`true` |')).toBe(true);
615620
});
621+
622+
it('should use the built-in example for nve-layout', () => {
623+
const attribute: Attribute = {
624+
name: 'nve-layout',
625+
description: 'Layout utility attribute',
626+
example: '',
627+
markdown: '',
628+
values: [{ name: 'row' }, { name: 'column' }]
629+
};
630+
631+
const markdown = attributeMetadataToMarkdown(attribute);
632+
633+
expect(markdown).toContain('## nve-layout');
634+
expect(markdown).toContain('nve-layout="row gap:sm"');
635+
expect(markdown).toContain('nve-layout="grid gap:sm span-items:6"');
636+
});
637+
638+
it('should use the built-in example for nve-text', () => {
639+
const attribute: Attribute = {
640+
name: 'nve-text',
641+
description: 'Typography utility attribute',
642+
example: '',
643+
markdown: '',
644+
values: [{ name: 'heading' }, { name: 'body' }]
645+
};
646+
647+
const markdown = attributeMetadataToMarkdown(attribute);
648+
649+
expect(markdown).toContain('## nve-text');
650+
expect(markdown).toContain('nve-text="heading"');
651+
expect(markdown).toContain('nve-text="monospace"');
652+
});
653+
});
654+
655+
describe('searchContextAPIs', () => {
656+
beforeEach(() => {
657+
vi.clearAllMocks();
658+
});
659+
660+
it('should attach markdown to attribute results that have values', async () => {
661+
const { ApiService } = await import('@internals/metadata');
662+
vi.mocked(ApiService.search).mockResolvedValue([
663+
{ name: 'nve-layout', description: 'Layout utility', values: [{ name: 'row' }], markdown: '' }
664+
] as never);
665+
666+
const results = (await searchContextAPIs('layout')) as Attribute[];
667+
668+
expect(results).toHaveLength(1);
669+
expect(results[0].markdown).toContain('## nve-layout');
670+
});
671+
672+
it('should leave element results without a markdown field untouched', async () => {
673+
const { ApiService } = await import('@internals/metadata');
674+
vi.mocked(ApiService.search).mockResolvedValue([
675+
{ name: 'nve-button', manifest: { metadata: { markdown: 'x' } } }
676+
] as never);
677+
678+
const results = (await searchContextAPIs('button')) as Element[];
679+
680+
expect(results).toHaveLength(1);
681+
expect((results[0] as Attribute).markdown).toBeUndefined();
682+
});
683+
684+
it('should limit results to the configured limit', async () => {
685+
const { ApiService } = await import('@internals/metadata');
686+
vi.mocked(ApiService.search).mockResolvedValue(
687+
Array.from({ length: 5 }, (_, index) => ({ name: `nve-item-${index}` })) as never
688+
);
689+
690+
const results = await searchContextAPIs('item', { limit: 2 });
691+
692+
expect(results).toHaveLength(2);
693+
});
694+
695+
it('should return every result when no limit is provided', async () => {
696+
const { ApiService } = await import('@internals/metadata');
697+
vi.mocked(ApiService.search).mockResolvedValue(
698+
Array.from({ length: 5 }, (_, index) => ({ name: `nve-item-${index}` })) as never
699+
);
700+
701+
const results = await searchContextAPIs('item', {});
702+
703+
expect(results).toHaveLength(5);
704+
});
616705
});

‎projects/internals/tools/src/distill/examples.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ describe('isContextExample', () => {
128128
})
129129
).toBe(false);
130130
});
131+
132+
it('should treat an example with no id, tags, or element as a default', () => {
133+
expect(isContextExample({})).toBe(true);
134+
});
131135
});
132136

133137
describe('rankExample', () => {
@@ -147,6 +151,10 @@ describe('rankExample', () => {
147151
expect(rankExample({ id: 'button-default' })).toBe(3);
148152
});
149153

154+
it('should default to the lowest rank when the id is missing', () => {
155+
expect(rankExample({})).toBe(3);
156+
});
157+
150158
it('should strip elements- prefix before ranking', () => {
151159
expect(rankExample({ id: 'elements-template-foo' })).toBe(0);
152160
expect(rankExample({ id: 'elements-pattern-form' })).toBe(1);
@@ -267,4 +275,18 @@ describe('distillExamples', () => {
267275
expect(result).toHaveLength(1);
268276
expect(result[0].summary).toBe('Has summary');
269277
});
278+
279+
it('should default every shaped field when examples omit them', () => {
280+
const result = distillExamples([{}, {}]);
281+
282+
expect(result).toHaveLength(2);
283+
expect(result[0]).toEqual({ id: '', name: '', summary: '', element: '', template: '' });
284+
});
285+
286+
it('should fall back to the description when the summary is missing', () => {
287+
const result = distillExamples([{ id: 'widget', element: 'nve-widget', description: 'Reusable widget' }]);
288+
289+
expect(result).toHaveLength(1);
290+
expect(result[0].summary).toBe('Reusable widget');
291+
});
270292
});

‎projects/internals/tools/src/internal/node.test.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,68 @@ describe('internal/node', () => {
162162

163163
expect(result).toEqual([]);
164164
});
165+
166+
it('should ignore PATH entries that are not regular files', async () => {
167+
const { existsSync, statSync } = await import('node:fs');
168+
vi.mocked(existsSync).mockReturnValue(true);
169+
vi.mocked(statSync).mockReturnValue({ isFile: () => false } as ReturnType<typeof statSync>);
170+
171+
const { findExecutablesOnPath } = await import('./node.js');
172+
const result = findExecutablesOnPath('nve', { envPath: '/a' });
173+
174+
expect(result).toEqual([]);
175+
});
176+
177+
it('should expand the command with explicit PATHEXT extensions on win32', async () => {
178+
const { existsSync, statSync, realpathSync } = await import('node:fs');
179+
vi.mocked(existsSync).mockReturnValue(true);
180+
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
181+
vi.mocked(realpathSync).mockImplementation(path => path.toString());
182+
183+
const { findExecutablesOnPath } = await import('./node.js');
184+
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32', pathExt: '.EXE;.CMD' });
185+
186+
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.exe'))).toBe(true);
187+
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.cmd'))).toBe(true);
188+
});
189+
190+
it('should fall back to the PATHEXT environment variable on win32', async () => {
191+
const { existsSync, statSync, realpathSync } = await import('node:fs');
192+
vi.mocked(existsSync).mockReturnValue(true);
193+
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
194+
vi.mocked(realpathSync).mockImplementation(path => path.toString());
195+
vi.stubEnv('PATHEXT', '.BAT');
196+
197+
const { findExecutablesOnPath } = await import('./node.js');
198+
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });
199+
200+
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
201+
vi.unstubAllEnvs();
202+
});
203+
204+
it('should treat any matching file as executable on win32 without an access check', async () => {
205+
const { accessSync, existsSync, statSync, realpathSync } = await import('node:fs');
206+
vi.mocked(existsSync).mockReturnValue(true);
207+
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
208+
vi.mocked(realpathSync).mockImplementation(path => path.toString());
209+
210+
const { findExecutablesOnPath } = await import('./node.js');
211+
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32', pathExt: '.EXE' });
212+
213+
expect(result.length).toBeGreaterThan(0);
214+
expect(accessSync).not.toHaveBeenCalled();
215+
});
216+
217+
it('should default to process PATH and platform when options are omitted', async () => {
218+
const { existsSync } = await import('node:fs');
219+
vi.mocked(existsSync).mockReturnValue(false);
220+
vi.stubEnv('PATH', '/usr/bin:/bin');
221+
222+
const { findExecutablesOnPath } = await import('./node.js');
223+
const result = findExecutablesOnPath('definitely-not-a-real-command');
224+
225+
expect(result).toEqual([]);
226+
vi.unstubAllEnvs();
227+
});
165228
});
166229
});

‎projects/internals/tools/src/internal/utils.test.ts‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { describe, expect, it } from 'vitest';
4+
import { afterEach, describe, expect, it, vi } from 'vitest';
55
import type { ProjectElement } from '@internals/metadata';
6-
import { getElementImports, getAvailableElementTags, wrapText } from './utils.js';
6+
import { getElementImports, getAvailableElementTags, isDebug, wrapText } from './utils.js';
77

88
describe('getElementImports', () => {
99
const elements: ProjectElement[] = [
@@ -124,4 +124,33 @@ describe('wrapText', () => {
124124
const result = wrapText(text, 10);
125125
expect(result).toContain('superlongwordthatexceedswidth');
126126
});
127+
128+
it('should handle a single word longer than width with nothing trailing', () => {
129+
const text = 'superlongwordthatexceedswidth';
130+
expect(wrapText(text, 10)).toBe('superlongwordthatexceedswidth');
131+
});
132+
});
133+
134+
describe('isDebug', () => {
135+
afterEach(() => {
136+
vi.unstubAllEnvs();
137+
});
138+
139+
it('should return true when debug is enabled outside of the mcp environment', () => {
140+
vi.stubEnv('ELEMENTS_DEBUG', 'true');
141+
vi.stubEnv('ELEMENTS_ENV', 'cli');
142+
expect(isDebug()).toBe(true);
143+
});
144+
145+
it('should return false when debug is not enabled', () => {
146+
vi.stubEnv('ELEMENTS_DEBUG', 'false');
147+
vi.stubEnv('ELEMENTS_ENV', 'cli');
148+
expect(isDebug()).toBe(false);
149+
});
150+
151+
it('should return false in the mcp environment even when debug is enabled', () => {
152+
vi.stubEnv('ELEMENTS_DEBUG', 'true');
153+
vi.stubEnv('ELEMENTS_ENV', 'mcp');
154+
expect(isDebug()).toBe(false);
155+
});
127156
});

‎projects/internals/tools/src/packages/utils.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import { describe, expect, it, vi } from 'vitest';
55
import {
66
findPublicAPIChangelog,
7+
getAvailablePackages,
78
getPackage,
9+
getVersions,
810
hasChangelogEntries,
911
limitChangelogVersions,
1012
scopeOrder
@@ -198,4 +200,47 @@ describe('getPackage', () => {
198200
expect(result).toContain('# @nvidia-elements/core v1.0.0');
199201
expect(result).toContain('No readme pkg');
200202
});
203+
204+
it('should throw with no available packages when metadata is missing', async () => {
205+
const { ProjectsService } = await import('@internals/metadata');
206+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefined as never);
207+
208+
await expect(getPackage('@nvidia-elements/core')).rejects.toThrow('No package found for "@nvidia-elements/core"');
209+
});
210+
});
211+
212+
describe('getVersions', () => {
213+
it('should resolve the latest published versions for available packages', async () => {
214+
const result = await getVersions();
215+
expect(result).toEqual({
216+
'@nvidia-elements/core': '2.0.0',
217+
'@nvidia-elements/themes': '1.5.0'
218+
});
219+
});
220+
221+
it('should default to an empty package list when metadata is missing', async () => {
222+
const { ProjectsService } = await import('@internals/metadata');
223+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefined as never);
224+
225+
const result = await getVersions();
226+
expect(result).toBeDefined();
227+
});
228+
});
229+
230+
describe('getAvailablePackages', () => {
231+
it('should format available packages as markdown sections', async () => {
232+
const result = await getAvailablePackages();
233+
expect(result).toContain('## @nvidia-elements/core v2.0.0');
234+
expect(result).toContain('Core elements');
235+
expect(result).toContain('## @nvidia-elements/themes v1.5.0');
236+
expect(result).toContain('Theme tokens');
237+
});
238+
239+
it('should return an empty string when metadata is missing', async () => {
240+
const { ProjectsService } = await import('@internals/metadata');
241+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefined as never);
242+
243+
const result = await getAvailablePackages();
244+
expect(result).toBe('');
245+
});
201246
});

‎projects/internals/tools/src/project/setup-agent.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ describe('setup-mcp', () => {
171171

172172
const written = vi.mocked(writeFileSync).mock.calls[0][1] as string;
173173
expect(written).toContain('[mcp_servers.elements]');
174+
expect(written).toContain(
175+
'description = "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples"'
176+
);
174177
expect(written).toContain('command = "nve"');
175178
expect(written).toContain('args = ["mcp"]');
176179
});

‎projects/internals/tools/src/project/setup-agent.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function writeMcpTomlConfig(configPath: string): string {
6565

6666
const sectionRegex = new RegExp(`\\[mcp_servers\\.elements\\][\\s\\S]*?(?=\\n\\[|$)`);
6767
const content = existing.replace(sectionRegex, '').trimEnd();
68-
const block = `\n\n[mcp_servers.elements]\ncommand = "nve"\nargs = ["mcp"]\n`;
68+
const block = `\n\n[mcp_servers.elements]\ndescription = "${DESCRIPTION}"\ncommand = "nve"\nargs = ["mcp"]\n`;
6969
const updated = (content + block).trimStart();
7070
const dir = configPath.substring(0, configPath.lastIndexOf('/'));
7171

0 commit comments

Comments
 (0)