diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 0d4f7961..832be130 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -120,6 +120,27 @@ describe('validate', () => { expect(result.error?.includes('Invalid language')).toBeTruthy(); }); + // Case-insensitive flag values + it('accepts lowercase flag values and normalizes them', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsByo, + framework: 'strands' as any, + modelProvider: 'bedrock' as any, + language: 'python' as any, + }); + expect(result.valid).toBe(true); + }); + + it('accepts uppercase flag values and normalizes them', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsByo, + framework: 'STRANDS' as any, + modelProvider: 'BEDROCK' as any, + language: 'PYTHON' as any, + }); + expect(result.valid).toBe(true); + }); + // AC3: Framework/model provider compatibility it('returns error for incompatible framework and model provider', () => { const result = validateAddAgentOptions({ diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 0aac0a21..fd704c71 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -7,6 +7,7 @@ import { SDKFrameworkSchema, TargetLanguageSchema, getSupportedModelProviders, + matchEnumValue, } from '../../../schema'; import { getExistingGateways } from '../../operations/mcp/create-mcp'; import type { @@ -58,6 +59,19 @@ async function validateCredentialExists(credentialName: string): Promise { + // Normalize enum flag values (case-insensitive matching) + if (options.language) + options.language = + (matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language; + if (!options.name) { return { valid: false, error: '--name is required' }; } diff --git a/src/cli/commands/create/__tests__/validate.test.ts b/src/cli/commands/create/__tests__/validate.test.ts index a33b0609..8137f5d7 100644 --- a/src/cli/commands/create/__tests__/validate.test.ts +++ b/src/cli/commands/create/__tests__/validate.test.ts @@ -132,6 +132,22 @@ describe('validateCreateOptions', () => { expect(result.valid).toBe(true); }); + it('accepts lowercase flag values and normalizes them', () => { + const result = validateCreateOptions( + { name: 'TestProjLower', language: 'python', framework: 'strands', modelProvider: 'bedrock', memory: 'none' }, + testDir + ); + expect(result.valid).toBe(true); + }); + + it('accepts uppercase flag values and normalizes them', () => { + const result = validateCreateOptions( + { name: 'TestProjUpper', language: 'PYTHON', framework: 'STRANDS', modelProvider: 'BEDROCK', memory: 'none' }, + testDir + ); + expect(result.valid).toBe(true); + }); + it('returns invalid for unsupported framework/model combination', () => { // GoogleADK only supports certain providers, not all const result = validateCreateOptions( diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index 635fa16c..6c3725f8 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -5,6 +5,7 @@ import { SDKFrameworkSchema, TargetLanguageSchema, getSupportedModelProviders, + matchEnumValue, } from '../../../schema'; import type { CreateOptions } from './types'; import { existsSync } from 'fs'; @@ -50,6 +51,13 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val return { valid: true }; } + // Normalize enum flag values (case-insensitive matching) + if (options.language) options.language = matchEnumValue(TargetLanguageSchema, options.language) ?? options.language; + if (options.framework) options.framework = matchEnumValue(SDKFrameworkSchema, options.framework) ?? options.framework; + if (options.modelProvider) + options.modelProvider = matchEnumValue(ModelProviderSchema, options.modelProvider) ?? options.modelProvider; + if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build; + // Validate build type if provided if (options.build) { const buildResult = BuildTypeSchema.safeParse(options.build); diff --git a/src/schema/__tests__/constants.test.ts b/src/schema/__tests__/constants.test.ts index 653bef11..f2d1793e 100644 --- a/src/schema/__tests__/constants.test.ts +++ b/src/schema/__tests__/constants.test.ts @@ -6,12 +6,35 @@ import { RESERVED_PROJECT_NAMES, RuntimeVersionSchema, SDKFrameworkSchema, + TargetLanguageSchema, getSupportedModelProviders, isModelProviderSupported, isReservedProjectName, + matchEnumValue, } from '../constants.js'; import { describe, expect, it } from 'vitest'; +describe('matchEnumValue', () => { + it('returns canonical value for case-insensitive match', () => { + expect(matchEnumValue(SDKFrameworkSchema, 'strands')).toBe('Strands'); + expect(matchEnumValue(SDKFrameworkSchema, 'STRANDS')).toBe('Strands'); + expect(matchEnumValue(SDKFrameworkSchema, 'Strands')).toBe('Strands'); + expect(matchEnumValue(ModelProviderSchema, 'bedrock')).toBe('Bedrock'); + expect(matchEnumValue(TargetLanguageSchema, 'python')).toBe('Python'); + }); + + it('returns undefined for non-matching input', () => { + expect(matchEnumValue(SDKFrameworkSchema, 'nonexistent')).toBeUndefined(); + expect(matchEnumValue(ModelProviderSchema, 'azure')).toBeUndefined(); + }); + + it('handles multi-word enum values', () => { + expect(matchEnumValue(SDKFrameworkSchema, 'langchain_langgraph')).toBe('LangChain_LangGraph'); + expect(matchEnumValue(SDKFrameworkSchema, 'openaiagents')).toBe('OpenAIAgents'); + expect(matchEnumValue(SDKFrameworkSchema, 'googleadk')).toBe('GoogleADK'); + }); +}); + describe('SDKFrameworkSchema', () => { it.each(['Strands', 'LangChain_LangGraph', 'CrewAI', 'GoogleADK', 'OpenAIAgents'])('accepts "%s"', framework => { expect(SDKFrameworkSchema.safeParse(framework).success).toBe(true); diff --git a/src/schema/constants.ts b/src/schema/constants.ts index 1b5d27d8..723d23da 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -13,6 +13,15 @@ export type TargetLanguage = z.infer; export const ModelProviderSchema = z.enum(['Bedrock', 'Gemini', 'OpenAI', 'Anthropic']); export type ModelProvider = z.infer; +/** + * Case-insensitively match a user-provided value against a Zod enum's options. + * Returns the canonical (correctly-cased) value, or undefined if no match. + */ +export function matchEnumValue(schema: { options: readonly string[] }, input: string): string | undefined { + const lower = input.toLowerCase(); + return schema.options.find(v => v.toLowerCase() === lower); +} + /** * Default model IDs used for each provider. * These are the models generated in agent templates.