Skip to content

Commit a7ad4f4

Browse files
committed
fix: make CLI flag values case-insensitive
1 parent 47da675 commit a7ad4f4

6 files changed

Lines changed: 92 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
@@ -113,6 +113,27 @@ describe('validate', () => {
113113
expect(result.error?.includes('Invalid language')).toBeTruthy();
114114
});
115115

116+
// Case-insensitive flag values
117+
it('accepts lowercase flag values and normalizes them', () => {
118+
const result = validateAddAgentOptions({
119+
...validAgentOptionsByo,
120+
framework: 'strands' as any,
121+
modelProvider: 'bedrock' as any,
122+
language: 'python' as any,
123+
});
124+
expect(result.valid).toBe(true);
125+
});
126+
127+
it('accepts uppercase flag values and normalizes them', () => {
128+
const result = validateAddAgentOptions({
129+
...validAgentOptionsByo,
130+
framework: 'STRANDS' as any,
131+
modelProvider: 'BEDROCK' as any,
132+
language: 'PYTHON' as any,
133+
});
134+
expect(result.valid).toBe(true);
135+
});
136+
116137
// AC3: Framework/model provider compatibility
117138
it('returns error for incompatible framework and model provider', () => {
118139
const result = validateAddAgentOptions({

src/cli/commands/add/validate.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SDKFrameworkSchema,
77
TargetLanguageSchema,
88
getSupportedModelProviders,
9+
matchEnumValue,
910
} from '../../../schema';
1011
import type {
1112
AddAgentOptions,
@@ -27,6 +28,16 @@ const VALID_STRATEGIES = ['SEMANTIC', 'SUMMARIZATION', 'USER_PREFERENCE'];
2728

2829
// Agent validation
2930
export function validateAddAgentOptions(options: AddAgentOptions): ValidationResult {
31+
// Normalize enum flag values (case-insensitive matching)
32+
if (options.framework)
33+
options.framework = (matchEnumValue(SDKFrameworkSchema, options.framework) as typeof options.framework) ?? options.framework;
34+
if (options.modelProvider)
35+
options.modelProvider =
36+
(matchEnumValue(ModelProviderSchema, options.modelProvider) as typeof options.modelProvider) ?? options.modelProvider;
37+
if (options.language)
38+
options.language = (matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;
39+
if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build;
40+
3041
if (!options.name) {
3142
return { valid: false, error: '--name is required' };
3243
}
@@ -155,6 +166,10 @@ export function validateAddGatewayOptions(options: AddGatewayOptions): Validatio
155166

156167
// MCP Tool validation
157168
export function validateAddMcpToolOptions(options: AddMcpToolOptions): ValidationResult {
169+
// Normalize enum flag values (case-insensitive matching)
170+
if (options.language)
171+
options.language = (matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;
172+
158173
if (!options.name) {
159174
return { valid: false, error: '--name is required' };
160175
}

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)