diff --git a/src/cli/commands/add/__tests__/actions.test.ts b/src/cli/commands/add/__tests__/actions.test.ts index 852523bc..0fffde89 100644 --- a/src/cli/commands/add/__tests__/actions.test.ts +++ b/src/cli/commands/add/__tests__/actions.test.ts @@ -1,6 +1,15 @@ import { buildGatewayTargetConfig } from '../actions.js'; import type { ValidatedAddGatewayTargetOptions } from '../actions.js'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mockCreateToolFromWizard = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '/tmp' }); +const mockCreateExternalGatewayTarget = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '' }); + +vi.mock('../../../operations/mcp/create-mcp', () => ({ + createToolFromWizard: (...args: unknown[]) => mockCreateToolFromWizard(...args), + createExternalGatewayTarget: (...args: unknown[]) => mockCreateExternalGatewayTarget(...args), + createGatewayFromWizard: vi.fn(), +})); describe('buildGatewayTargetConfig', () => { it('maps name, gateway, language correctly', () => { @@ -66,3 +75,26 @@ describe('buildGatewayTargetConfig', () => { expect(config.outboundAuth).toBeUndefined(); }); }); + +// Dynamic import to pick up mocks +const { handleAddGatewayTarget } = await import('../actions.js'); + +describe('handleAddGatewayTarget', () => { + afterEach(() => vi.clearAllMocks()); + + it('routes existing-endpoint to createExternalGatewayTarget', async () => { + const options: ValidatedAddGatewayTargetOptions = { + name: 'test-tool', + language: 'Other', + host: 'Lambda', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', + gateway: 'my-gw', + }; + + await handleAddGatewayTarget(options); + + expect(mockCreateExternalGatewayTarget).toHaveBeenCalledOnce(); + expect(mockCreateToolFromWizard).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index e9a7992a..40f5dfec 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -422,6 +422,18 @@ describe('validate', () => { expect(result.valid).toBe(false); expect(result.error).toContain('--credential-name is required'); }); + + it('rejects --host with existing-endpoint', async () => { + const options: AddGatewayTargetOptions = { + name: 'test-tool', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', + host: 'Lambda', + }; + const result = await validateAddGatewayTargetOptions(options); + expect(result.valid).toBe(false); + expect(result.error).toBe('--host is not applicable for existing endpoint targets'); + }); }); describe('validateAddMemoryOptions', () => { diff --git a/src/cli/commands/add/actions.ts b/src/cli/commands/add/actions.ts index 6d28bfc7..0e2543f9 100644 --- a/src/cli/commands/add/actions.ts +++ b/src/cli/commands/add/actions.ts @@ -23,7 +23,11 @@ import { createCredential, resolveCredentialStrategy, } from '../../operations/identity/create-identity'; -import { createGatewayFromWizard, createToolFromWizard } from '../../operations/mcp/create-mcp'; +import { + createExternalGatewayTarget, + createGatewayFromWizard, + createToolFromWizard, +} from '../../operations/mcp/create-mcp'; import { createMemory } from '../../operations/memory/create-memory'; import { createRenderer } from '../../templates'; import type { MemoryOption } from '../../tui/screens/generate/types'; @@ -334,6 +338,10 @@ export async function handleAddGatewayTarget( } const config = buildGatewayTargetConfig(options); + if (config.source === 'existing-endpoint') { + const result = await createExternalGatewayTarget(config); + return { success: true, toolName: result.toolName }; + } const result = await createToolFromWizard(config); return { success: true, toolName: result.toolName, sourcePath: result.projectPath }; } catch (err) { diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index d7cbc802..2731ed83 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -198,6 +198,9 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO } if (options.source === 'existing-endpoint') { + if (options.host) { + return { valid: false, error: '--host is not applicable for existing endpoint targets' }; + } if (!options.endpoint) { return { valid: false, error: '--endpoint is required when source is existing-endpoint' }; } diff --git a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx index f8a2522e..0c811d43 100644 --- a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx @@ -4,14 +4,8 @@ import type { SelectableItem } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation } from '../../hooks'; import { generateUniqueName } from '../../utils'; -import type { AddGatewayTargetConfig, ComputeHost, TargetLanguage } from './types'; -import { - COMPUTE_HOST_OPTIONS, - MCP_TOOL_STEP_LABELS, - SKIP_FOR_NOW, - SOURCE_OPTIONS, - TARGET_LANGUAGE_OPTIONS, -} from './types'; +import type { AddGatewayTargetConfig } from './types'; +import { MCP_TOOL_STEP_LABELS, SKIP_FOR_NOW } from './types'; import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard'; import { Box, Text } from 'ink'; import React, { useMemo } from 'react'; @@ -31,16 +25,6 @@ export function AddGatewayTargetScreen({ }: AddGatewayTargetScreenProps) { const wizard = useAddGatewayTargetWizard(existingGateways); - const sourceItems: SelectableItem[] = useMemo( - () => SOURCE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - - const languageItems: SelectableItem[] = useMemo( - () => TARGET_LANGUAGE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - const gatewayItems: SelectableItem[] = useMemo( () => [ ...existingGateways.map(g => ({ id: g, title: g })), @@ -49,33 +33,11 @@ export function AddGatewayTargetScreen({ [existingGateways] ); - const hostItems: SelectableItem[] = useMemo( - () => COMPUTE_HOST_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - - const isSourceStep = wizard.step === 'source'; - const isLanguageStep = wizard.step === 'language'; const isGatewayStep = wizard.step === 'gateway'; - const isHostStep = wizard.step === 'host'; const isTextStep = wizard.step === 'name' || wizard.step === 'endpoint'; const isConfirmStep = wizard.step === 'confirm'; const noGatewaysAvailable = isGatewayStep && existingGateways.length === 0; - const sourceNav = useListNavigation({ - items: sourceItems, - onSelect: item => wizard.setSource(item.id as 'existing-endpoint' | 'create-new'), - onExit: () => wizard.goBack(), - isActive: isSourceStep, - }); - - const languageNav = useListNavigation({ - items: languageItems, - onSelect: item => wizard.setLanguage(item.id as TargetLanguage), - onExit: () => onExit(), - isActive: isLanguageStep, - }); - const gatewayNav = useListNavigation({ items: gatewayItems, onSelect: item => wizard.setGateway(item.id), @@ -83,13 +45,6 @@ export function AddGatewayTargetScreen({ isActive: isGatewayStep && !noGatewaysAvailable, }); - const hostNav = useListNavigation({ - items: hostItems, - onSelect: item => wizard.setHost(item.id as ComputeHost), - onExit: () => wizard.goBack(), - isActive: isHostStep, - }); - useListNavigation({ items: [{ id: 'confirm', title: 'Confirm' }], onSelect: () => onComplete(wizard.config), @@ -108,19 +63,6 @@ export function AddGatewayTargetScreen({ return ( - {isSourceStep && ( - - )} - - {isLanguageStep && ( - - )} - {isGatewayStep && !noGatewaysAvailable && ( } - {isHostStep && ( - - )} - {isTextStep && ( (getDefaultConfig); const [step, setStep] = useState('name'); - const steps = useMemo(() => getSteps(config.source), [config.source]); + const steps = useMemo(() => getSteps(), []); const currentIndex = steps.indexOf(step); const goBack = useCallback(() => { - // Recalculate steps in case source changed - const currentSteps = getSteps(config.source); + const currentSteps = getSteps(); const idx = currentSteps.indexOf(step); const prevStep = currentSteps[idx - 1]; if (prevStep) setStep(prevStep); - }, [config.source, step]); + }, [step]); const setName = useCallback((name: string) => { setConfig(c => ({ @@ -58,19 +54,7 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { sourcePath: `${APP_DIR}/${MCP_APP_SUBDIR}/${name}`, toolDefinition: deriveToolDefinition(name), })); - setStep('source'); - }, []); - - const setSource = useCallback((source: 'existing-endpoint' | 'create-new') => { - setConfig(c => ({ - ...c, - source, - })); - if (source === 'existing-endpoint') { - setStep('endpoint'); - } else { - setStep('language'); - } + setStep('endpoint'); }, []); const setEndpoint = useCallback((endpoint: string) => { @@ -81,35 +65,14 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { setStep('gateway'); }, []); - const setLanguage = useCallback((language: TargetLanguage) => { - setConfig(c => ({ - ...c, - language, - })); - setStep('gateway'); - }, []); - const setGateway = useCallback((gateway: string) => { setConfig(c => { - const isExternal = c.source === 'existing-endpoint'; const isSkipped = gateway === SKIP_FOR_NOW; - if (isExternal || isSkipped) { - setStep('confirm'); - } else { - setStep('host'); - } + setStep('confirm'); return { ...c, gateway: isSkipped ? undefined : gateway }; }); }, []); - const setHost = useCallback((host: ComputeHost) => { - setConfig(c => ({ - ...c, - host, - })); - setStep('confirm'); - }, []); - const reset = useCallback(() => { setConfig(getDefaultConfig()); setStep('name'); @@ -123,11 +86,8 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { existingGateways, goBack, setName, - setSource, setEndpoint, - setLanguage, setGateway, - setHost, reset, }; }