Skip to content

Commit 115fc75

Browse files
committed
fix(cli): remove TERMUI_REGISTRY env var and validate components before dep install
Replace the TERMUI_REGISTRY env-var override with the existing config-file mechanism — tests now inject the mock registry URL via termui.config.json (the registry field) instead of a process-level env var. Also move the "component not found" check to before the core-dep install step so that unknown component names fail fast without touching npm or the network. Bumps CLI to v1.4.3.
1 parent 5978ac3 commit 115fc75

4 files changed

Lines changed: 80 additions & 62 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "termui",
3-
"version": "1.4.2",
3+
"version": "1.4.3",
44
"description": "TermUI CLI — shadcn-style terminal UI component distribution",
55
"type": "module",
66
"bin": {

packages/cli/src/commands/add.integration.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ describe.skipIf(!built)('termui add (integration)', () => {
3535

3636
const fs = mockFS({
3737
'package.json': JSON.stringify({ name: 'test-app', type: 'module' }),
38-
'termui.config.json': JSON.stringify({ componentsDir: './components/ui' }),
38+
'termui.config.json': JSON.stringify({
39+
componentsDir: './components/ui',
40+
registry: registry.url,
41+
}),
3942
});
4043

4144
try {
4245
const { output, exitCode } = await testCLI(`${CLI_CMD} add --dry-run spinner`, {
4346
cwd: fs.root,
44-
env: { TERMUI_REGISTRY: registry.url },
4547
});
4648
expect(exitCode).toBe(0);
4749
expect(output.toLowerCase()).toContain('dry-run');
@@ -58,13 +60,15 @@ describe.skipIf(!built)('termui add (integration)', () => {
5860

5961
const fs = mockFS({
6062
'package.json': JSON.stringify({ name: 'test-app', type: 'module' }),
61-
'termui.config.json': JSON.stringify({ componentsDir: './components/ui' }),
63+
'termui.config.json': JSON.stringify({
64+
componentsDir: './components/ui',
65+
registry: registry.url,
66+
}),
6267
});
6368

6469
try {
6570
const { exitCode } = await testCLI(`${CLI_CMD} add nonexistent-component`, {
6671
cwd: fs.root,
67-
env: { TERMUI_REGISTRY: registry.url },
6872
});
6973
expect(exitCode).toBe(1);
7074
} finally {
@@ -78,13 +82,15 @@ describe.skipIf(!built)('termui add (integration)', () => {
7882

7983
const fs = mockFS({
8084
'package.json': JSON.stringify({ name: 'test-app', type: 'module' }),
81-
'termui.config.json': JSON.stringify({ componentsDir: './components/ui' }),
85+
'termui.config.json': JSON.stringify({
86+
componentsDir: './components/ui',
87+
registry: registry.url,
88+
}),
8289
});
8390

8491
try {
8592
const { exitCode, output } = await testCLI(`${CLI_CMD} add --recipe`, {
8693
cwd: fs.root,
87-
env: { TERMUI_REGISTRY: registry.url },
8894
});
8995
expect(exitCode).toBe(1);
9096
expect(output).toContain('recipe');
@@ -106,13 +112,15 @@ describe.skipIf(!built)('termui add (integration)', () => {
106112

107113
const fs = mockFS({
108114
'package.json': JSON.stringify({ name: 'test-app', type: 'module' }),
109-
'termui.config.json': JSON.stringify({ componentsDir: './components/ui' }),
115+
'termui.config.json': JSON.stringify({
116+
componentsDir: './components/ui',
117+
registry: registry.url,
118+
}),
110119
});
111120

112121
try {
113122
await testCLI(`${CLI_CMD} add --dry-run spinner`, {
114123
cwd: fs.root,
115-
env: { TERMUI_REGISTRY: registry.url },
116124
});
117125
// In a dry run no component file should be written
118126
expect(fs.exists('components/ui/feedback/Spinner.tsx')).toBe(false);

packages/cli/src/commands/add.ts

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -104,22 +104,22 @@ const CORE_DEPS = ['termui', 'react', 'ink'];
104104
* Map: adapter name the user types → npm package(s) to install.
105105
*/
106106
const ADAPTER_DEPS: Record<string, string[]> = {
107-
commander: ['commander'],
108-
chalk: ['chalk'],
109-
ora: ['ora'],
110-
meow: ['meow'],
111-
inquirer: ['inquirer'],
112-
yargs: ['yargs'],
113-
vue: ['vue'],
114-
svelte: ['svelte'],
115-
conf: ['conf'],
116-
execa: ['execa'],
117-
'node-pty': ['node-pty'],
118-
pty: ['node-pty'],
119-
keychain: ['keytar'],
120-
git: ['simple-git'],
121-
github: ['@octokit/rest'],
122-
ai: ['@anthropic-ai/sdk'],
107+
commander: ['commander'],
108+
chalk: ['chalk'],
109+
ora: ['ora'],
110+
meow: ['meow'],
111+
inquirer: ['inquirer'],
112+
yargs: ['yargs'],
113+
vue: ['vue'],
114+
svelte: ['svelte'],
115+
conf: ['conf'],
116+
execa: ['execa'],
117+
'node-pty': ['node-pty'],
118+
pty: ['node-pty'],
119+
keychain: ['keytar'],
120+
git: ['simple-git'],
121+
github: ['@octokit/rest'],
122+
ai: ['@anthropic-ai/sdk'],
123123
};
124124

125125
// ─── Prettier formatting (optional) ──────────────────────────────────────────
@@ -163,7 +163,7 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
163163

164164
const cwd = process.cwd();
165165
const config = getConfig(cwd);
166-
const registryUrl = process.env['TERMUI_REGISTRY'] ?? config.registry;
166+
const registryUrl = config.registry;
167167

168168
if (!opts?.isNested) {
169169
printLogo();
@@ -266,7 +266,7 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
266266
const config = getConfig(cwd);
267267

268268
// Build ordered registry URL list: primary first, then any extras from config.registries
269-
const primaryUrl = process.env['TERMUI_REGISTRY'] ?? config.registry;
269+
const primaryUrl = config.registry;
270270
const extraRegistries = (config.registries ?? []).filter((r) => r !== primaryUrl);
271271
const allRegistryUrls = [primaryUrl, ...extraRegistries];
272272

@@ -334,6 +334,42 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
334334
step(`${c.yellow}[dry-run]${c.reset} No files will be written`);
335335
}
336336

337+
const allComponentNames = Object.keys(registry.components);
338+
339+
// Validate all requested components exist before installing anything
340+
if (!installAll) {
341+
for (const componentName of targets) {
342+
if (ADAPTER_DEPS[componentName]) continue; // adapters are always valid
343+
if (!registry.components[componentName]) {
344+
fail(`No component ${bold(`'${componentName}'`)} found.`);
345+
346+
const closest = findClosestMatch(componentName, allComponentNames);
347+
if (closest && closest.distance <= 3) {
348+
console.log(`${c.yellow} Did you mean ${closest.name}?${c.reset}`);
349+
const closestMeta = registry.components[closest.name];
350+
if (closestMeta) {
351+
const sameCategory = allComponentNames.filter(
352+
(n) => registry.components[n]?.category === closestMeta.category
353+
);
354+
console.log(` Available in ${closestMeta.category}: ${sameCategory.join(', ')}`);
355+
}
356+
} else {
357+
const byCategory: Record<string, string[]> = {};
358+
for (const name of allComponentNames) {
359+
const cat = registry.components[name]?.category ?? 'other';
360+
if (!byCategory[cat]) byCategory[cat] = [];
361+
byCategory[cat]!.push(name);
362+
}
363+
for (const [cat, names] of Object.entries(byCategory)) {
364+
console.log(` Available in ${cat}: ${names.join(', ')}`);
365+
}
366+
}
367+
368+
process.exit(1);
369+
}
370+
}
371+
}
372+
337373
// Ensure core deps are installed for every component
338374
if (!isDryRun) {
339375
const missingCore = getMissingDeps(CORE_DEPS, cwd);
@@ -342,8 +378,6 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
342378
installDeps(missingCore, cwd);
343379
}
344380
}
345-
346-
const allComponentNames = Object.keys(registry.components);
347381
const installed = new Set<string>();
348382
let addedCount = 0;
349383
let existedCount = 0;
@@ -356,7 +390,9 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
356390
if (!isDryRun) {
357391
step(`Adapter ${hi(componentName)} → installing ${hi(adapterDeps.join(', '))}`);
358392
installDeps(adapterDeps, cwd);
359-
step(`${hi('◇')} termui/${componentName} ready ${dim(`import from 'termui/${componentName}'`)}`);
393+
step(
394+
`${hi('◇')} termui/${componentName} ready ${dim(`import from 'termui/${componentName}'`)}`
395+
);
360396
} else {
361397
step(`${c.yellow}[dry-run]${c.reset} Would install: ${adapterDeps.join(', ')}`);
362398
}
@@ -365,36 +401,7 @@ export async function add(args: string[], opts?: { isNested?: boolean }): Promis
365401
}
366402

367403
const meta = registry.components[componentName];
368-
if (!meta) {
369-
fail(`No component ${bold(`'${componentName}'`)} found.`);
370-
371-
// Fuzzy match: find closest candidate
372-
const closest = findClosestMatch(componentName, allComponentNames);
373-
if (closest && closest.distance <= 3) {
374-
console.log(`${c.yellow} Did you mean ${closest.name}?${c.reset}`);
375-
// Show all components in the same category as the closest match
376-
const closestMeta = registry.components[closest.name];
377-
if (closestMeta) {
378-
const sameCategory = allComponentNames.filter(
379-
(n) => registry.components[n]?.category === closestMeta.category
380-
);
381-
console.log(` Available in ${closestMeta.category}: ${sameCategory.join(', ')}`);
382-
}
383-
} else {
384-
// No close match — show all categories
385-
const byCategory: Record<string, string[]> = {};
386-
for (const name of allComponentNames) {
387-
const cat = registry.components[name]?.category ?? 'other';
388-
if (!byCategory[cat]) byCategory[cat] = [];
389-
byCategory[cat]!.push(name);
390-
}
391-
for (const [cat, names] of Object.entries(byCategory)) {
392-
console.log(` Available in ${cat}: ${names.join(', ')}`);
393-
}
394-
}
395-
396-
process.exit(1);
397-
}
404+
if (!meta) continue; // pre-flight check already caught unknown components
398405

399406
const result = await installComponent(
400407
meta,

packages/testing/src/cli.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,13 @@ export interface MockRegistryHandle {
116116
* { name: 'spinner', category: 'feedback', files: ['Spinner.tsx'],
117117
* source: { 'Spinner.tsx': 'export function Spinner() { return null; }' } }
118118
* ]);
119-
* const result = await testCLI(`node dist/cli.js add spinner`, {
120-
* env: { TERMUI_REGISTRY: registry.url }
119+
* const fs = mockFS({
120+
* 'package.json': JSON.stringify({ name: 'my-app', type: 'module' }),
121+
* 'termui.config.json': JSON.stringify({ componentsDir: './components/ui', registry: registry.url }),
121122
* });
123+
* const result = await testCLI(`node dist/cli.js add spinner`, { cwd: fs.root });
122124
* registry.cleanup();
125+
* fs.cleanup();
123126
* ```
124127
*/
125128
export function mockRegistry(components: MockRegistryComponent[]): MockRegistryHandle {

0 commit comments

Comments
 (0)