Skip to content

Commit c1144e0

Browse files
authored
fix: make CLI flag values case-insensitive (#413)
* fix: make CLI flag values case-insensitive * style: fix prettier formatting in add/validate.ts * chore: npm audit fix
1 parent ec41be7 commit c1144e0

6 files changed

Lines changed: 96 additions & 0 deletions

File tree

src/cli/commands/add/__tests__/validate.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ describe('validate', () => {
120120
expect(result.error?.includes('Invalid language')).toBeTruthy();
121121
});
122122

123+
// Case-insensitive flag values
124+
it('accepts lowercase flag values and normalizes them', () => {
125+
const result = validateAddAgentOptions({
126+
...validAgentOptionsByo,
127+
framework: 'strands' as any,
128+
modelProvider: 'bedrock' as any,
129+
language: 'python' as any,
130+
});
131+
expect(result.valid).toBe(true);
132+
});
133+
134+
it('accepts uppercase flag values and normalizes them', () => {
135+
const result = validateAddAgentOptions({
136+
...validAgentOptionsByo,
137+
framework: 'STRANDS' as any,
138+
modelProvider: 'BEDROCK' as any,
139+
language: 'PYTHON' as any,
140+
});
141+
expect(result.valid).toBe(true);
142+
});
143+
123144
// AC3: Framework/model provider compatibility
124145
it('returns error for incompatible framework and model provider', () => {
125146
const result = validateAddAgentOptions({

src/cli/commands/add/validate.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SDKFrameworkSchema,
88
TargetLanguageSchema,
99
getSupportedModelProviders,
10+
matchEnumValue,
1011
} from '../../../schema';
1112
import { getExistingGateways } from '../../operations/mcp/create-mcp';
1213
import type {
@@ -58,6 +59,19 @@ async function validateCredentialExists(credentialName: string): Promise<Validat
5859

5960
// Agent validation
6061
export function validateAddAgentOptions(options: AddAgentOptions): ValidationResult {
62+
// Normalize enum flag values (case-insensitive matching)
63+
if (options.framework)
64+
options.framework =
65+
(matchEnumValue(SDKFrameworkSchema, options.framework) as typeof options.framework) ?? options.framework;
66+
if (options.modelProvider)
67+
options.modelProvider =
68+
(matchEnumValue(ModelProviderSchema, options.modelProvider) as typeof options.modelProvider) ??
69+
options.modelProvider;
70+
if (options.language)
71+
options.language =
72+
(matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;
73+
if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build;
74+
6175
if (!options.name) {
6276
return { valid: false, error: '--name is required' };
6377
}
@@ -197,6 +211,11 @@ export function validateAddGatewayOptions(options: AddGatewayOptions): Validatio
197211

198212
// Gateway Target validation
199213
export async function validateAddGatewayTargetOptions(options: AddGatewayTargetOptions): Promise<ValidationResult> {
214+
// Normalize enum flag values (case-insensitive matching)
215+
if (options.language)
216+
options.language =
217+
(matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;
218+
200219
if (!options.name) {
201220
return { valid: false, error: '--name is required' };
202221
}

src/cli/commands/create/__tests__/validate.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ describe('validateCreateOptions', () => {
132132
expect(result.valid).toBe(true);
133133
});
134134

135+
it('accepts lowercase flag values and normalizes them', () => {
136+
const result = validateCreateOptions(
137+
{ name: 'TestProjLower', language: 'python', framework: 'strands', modelProvider: 'bedrock', memory: 'none' },
138+
testDir
139+
);
140+
expect(result.valid).toBe(true);
141+
});
142+
143+
it('accepts uppercase flag values and normalizes them', () => {
144+
const result = validateCreateOptions(
145+
{ name: 'TestProjUpper', language: 'PYTHON', framework: 'STRANDS', modelProvider: 'BEDROCK', memory: 'none' },
146+
testDir
147+
);
148+
expect(result.valid).toBe(true);
149+
});
150+
135151
it('returns invalid for unsupported framework/model combination', () => {
136152
// GoogleADK only supports certain providers, not all
137153
const result = validateCreateOptions(

src/cli/commands/create/validate.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SDKFrameworkSchema,
66
TargetLanguageSchema,
77
getSupportedModelProviders,
8+
matchEnumValue,
89
} from '../../../schema';
910
import type { CreateOptions } from './types';
1011
import { existsSync } from 'fs';
@@ -50,6 +51,13 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
5051
return { valid: true };
5152
}
5253

54+
// Normalize enum flag values (case-insensitive matching)
55+
if (options.language) options.language = matchEnumValue(TargetLanguageSchema, options.language) ?? options.language;
56+
if (options.framework) options.framework = matchEnumValue(SDKFrameworkSchema, options.framework) ?? options.framework;
57+
if (options.modelProvider)
58+
options.modelProvider = matchEnumValue(ModelProviderSchema, options.modelProvider) ?? options.modelProvider;
59+
if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build;
60+
5361
// Validate build type if provided
5462
if (options.build) {
5563
const buildResult = BuildTypeSchema.safeParse(options.build);

src/schema/__tests__/constants.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ import {
66
RESERVED_PROJECT_NAMES,
77
RuntimeVersionSchema,
88
SDKFrameworkSchema,
9+
TargetLanguageSchema,
910
getSupportedModelProviders,
1011
isModelProviderSupported,
1112
isReservedProjectName,
13+
matchEnumValue,
1214
} from '../constants.js';
1315
import { describe, expect, it } from 'vitest';
1416

17+
describe('matchEnumValue', () => {
18+
it('returns canonical value for case-insensitive match', () => {
19+
expect(matchEnumValue(SDKFrameworkSchema, 'strands')).toBe('Strands');
20+
expect(matchEnumValue(SDKFrameworkSchema, 'STRANDS')).toBe('Strands');
21+
expect(matchEnumValue(SDKFrameworkSchema, 'Strands')).toBe('Strands');
22+
expect(matchEnumValue(ModelProviderSchema, 'bedrock')).toBe('Bedrock');
23+
expect(matchEnumValue(TargetLanguageSchema, 'python')).toBe('Python');
24+
});
25+
26+
it('returns undefined for non-matching input', () => {
27+
expect(matchEnumValue(SDKFrameworkSchema, 'nonexistent')).toBeUndefined();
28+
expect(matchEnumValue(ModelProviderSchema, 'azure')).toBeUndefined();
29+
});
30+
31+
it('handles multi-word enum values', () => {
32+
expect(matchEnumValue(SDKFrameworkSchema, 'langchain_langgraph')).toBe('LangChain_LangGraph');
33+
expect(matchEnumValue(SDKFrameworkSchema, 'openaiagents')).toBe('OpenAIAgents');
34+
expect(matchEnumValue(SDKFrameworkSchema, 'googleadk')).toBe('GoogleADK');
35+
});
36+
});
37+
1538
describe('SDKFrameworkSchema', () => {
1639
it.each(['Strands', 'LangChain_LangGraph', 'CrewAI', 'GoogleADK', 'OpenAIAgents'])('accepts "%s"', framework => {
1740
expect(SDKFrameworkSchema.safeParse(framework).success).toBe(true);

src/schema/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export type TargetLanguage = z.infer<typeof TargetLanguageSchema>;
1313
export const ModelProviderSchema = z.enum(['Bedrock', 'Gemini', 'OpenAI', 'Anthropic']);
1414
export type ModelProvider = z.infer<typeof ModelProviderSchema>;
1515

16+
/**
17+
* Case-insensitively match a user-provided value against a Zod enum's options.
18+
* Returns the canonical (correctly-cased) value, or undefined if no match.
19+
*/
20+
export function matchEnumValue(schema: { options: readonly string[] }, input: string): string | undefined {
21+
const lower = input.toLowerCase();
22+
return schema.options.find(v => v.toLowerCase() === lower);
23+
}
24+
1625
/**
1726
* Default model IDs used for each provider.
1827
* These are the models generated in agent templates.

0 commit comments

Comments
 (0)