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
typePartialAPIResult
1213
}from'./utils.js';
1314

15+
vi.mock('@internals/metadata',()=>({
16+
ApiService: {search: vi.fn()}
17+
}));
18+
1419
describe('getPublishedPackageNames',()=>{
1520
constprojects=[
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+
constattribute: Attribute={
624+
name: 'nve-layout',
625+
description: 'Layout utility attribute',
626+
example: '',
627+
markdown: '',
628+
values: [{name: 'row'},{name: 'column'}]
629+
};
630+
631+
constmarkdown=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+
constattribute: Attribute={
640+
name: 'nve-text',
641+
description: 'Typography utility attribute',
642+
example: '',
643+
markdown: '',
644+
values: [{name: 'heading'},{name: 'body'}]
645+
};
646+
647+
constmarkdown=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 }=awaitimport('@internals/metadata');
662+
vi.mocked(ApiService.search).mockResolvedValue([
663+
{name: 'nve-layout',description: 'Layout utility',values: [{name: 'row'}],markdown: ''}
664+
]asnever);
665+
666+
constresults=(awaitsearchContextAPIs('layout'))asAttribute[];
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 }=awaitimport('@internals/metadata');
674+
vi.mocked(ApiService.search).mockResolvedValue([
675+
{name: 'nve-button',manifest: {metadata: {markdown: 'x'}}}
676+
]asnever);
677+
678+
constresults=(awaitsearchContextAPIs('button'))asElement[];
679+
680+
expect(results).toHaveLength(1);
681+
expect((results[0]asAttribute).markdown).toBeUndefined();
682+
});
683+
684+
it('should limit results to the configured limit',async()=>{
685+
const{ ApiService }=awaitimport('@internals/metadata');
686+
vi.mocked(ApiService.search).mockResolvedValue(
687+
Array.from({length: 5},(_,index)=>({name: `nve-item-${index}`}))asnever
688+
);
689+
690+
constresults=awaitsearchContextAPIs('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 }=awaitimport('@internals/metadata');
697+
vi.mocked(ApiService.search).mockResolvedValue(
698+
Array.from({length: 5},(_,index)=>({name: `nve-item-${index}`}))asnever
699+
);
700+
701+
constresults=awaitsearchContextAPIs('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+
constresult=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+
constresult=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 }=awaitimport('node:fs');
168+
vi.mocked(existsSync).mockReturnValue(true);
169+
vi.mocked(statSync).mockReturnValue({isFile: ()=>false}asReturnType<typeofstatSync>);
170+
171+
const{ findExecutablesOnPath }=awaitimport('./node.js');
172+
constresult=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 }=awaitimport('node:fs');
179+
vi.mocked(existsSync).mockReturnValue(true);
180+
vi.mocked(statSync).mockReturnValue({isFile: ()=>true}asReturnType<typeofstatSync>);
181+
vi.mocked(realpathSync).mockImplementation(path=>path.toString());
182+
183+
const{ findExecutablesOnPath }=awaitimport('./node.js');
184+
constresult=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 }=awaitimport('node:fs');
192+
vi.mocked(existsSync).mockReturnValue(true);
193+
vi.mocked(statSync).mockReturnValue({isFile: ()=>true}asReturnType<typeofstatSync>);
194+
vi.mocked(realpathSync).mockImplementation(path=>path.toString());
195+
vi.stubEnv('PATHEXT','.BAT');
196+
197+
const{ findExecutablesOnPath }=awaitimport('./node.js');
198+
constresult=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 }=awaitimport('node:fs');
206+
vi.mocked(existsSync).mockReturnValue(true);
207+
vi.mocked(statSync).mockReturnValue({isFile: ()=>true}asReturnType<typeofstatSync>);
208+
vi.mocked(realpathSync).mockImplementation(path=>path.toString());
209+
210+
const{ findExecutablesOnPath }=awaitimport('./node.js');
211+
constresult=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 }=awaitimport('node:fs');
219+
vi.mocked(existsSync).mockReturnValue(false);
220+
vi.stubEnv('PATH','/usr/bin:/bin');
221+
222+
const{ findExecutablesOnPath }=awaitimport('./node.js');
223+
constresult=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
importtype{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
constelements: ProjectElement[]=[
@@ -124,4 +124,33 @@ describe('wrapText', () => {
124124
constresult=wrapText(text,10);
125125
expect(result).toContain('superlongwordthatexceedswidth');
126126
});
127+
128+
it('should handle a single word longer than width with nothing trailing',()=>{
129+
consttext='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 }=awaitimport('@internals/metadata');
206+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefinedasnever);
207+
208+
awaitexpect(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+
constresult=awaitgetVersions();
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 }=awaitimport('@internals/metadata');
223+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefinedasnever);
224+
225+
constresult=awaitgetVersions();
226+
expect(result).toBeDefined();
227+
});
228+
});
229+
230+
describe('getAvailablePackages',()=>{
231+
it('should format available packages as markdown sections',async()=>{
232+
constresult=awaitgetAvailablePackages();
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 }=awaitimport('@internals/metadata');
241+
vi.mocked(ProjectsService.getData).mockResolvedValueOnce(undefinedasnever);
242+
243+
constresult=awaitgetAvailablePackages();
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
constwritten=vi.mocked(writeFileSync).mock.calls[0][1]asstring;
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
constsectionRegex=newRegExp(`\\[mcp_servers\\.elements\\][\\s\\S]*?(?=\\n\\[|$)`);
6767
constcontent=existing.replace(sectionRegex,'').trimEnd();
68-
constblock=`\n\n[mcp_servers.elements]\ncommand = "nve"\nargs = ["mcp"]\n`;
68+
constblock=`\n\n[mcp_servers.elements]\ndescription = "${DESCRIPTION}"\ncommand = "nve"\nargs = ["mcp"]\n`;
6969
constupdated=(content+block).trimStart();
7070
constdir=configPath.substring(0,configPath.lastIndexOf('/'));
7171

0 commit comments

Comments
 (0)