From 4e81252c8c19600ddd3875b12dc263dfc422aca9 Mon Sep 17 00:00:00 2001 From: huanshenyi Date: Sun, 31 May 2026 01:15:42 +0900 Subject: [PATCH] feat: add Mastra TypeScript framework template --- .../assets.snapshot.test.ts.snap | 281 ++++++++++++++++++ .../typescript/http/mastra/base/README.md | 17 ++ .../http/mastra/base/gitignore.template | 4 + .../typescript/http/mastra/base/main.ts | 37 +++ .../typescript/http/mastra/base/model/load.ts | 138 +++++++++ .../typescript/http/mastra/base/package.json | 36 +++ .../typescript/http/mastra/base/tsconfig.json | 19 ++ .../commands/add/__tests__/validate.test.ts | 8 + src/cli/commands/add/validate.ts | 10 +- .../create/__tests__/validate.test.ts | 14 + src/cli/commands/create/command.tsx | 2 +- src/cli/commands/create/validate.ts | 6 +- src/cli/primitives/AgentPrimitive.tsx | 2 +- src/cli/telemetry/schemas/common-shapes.ts | 9 +- src/cli/templates/MastraRenderer.ts | 9 + .../templates/__tests__/BaseRenderer.test.ts | 10 + src/cli/templates/index.ts | 4 + .../tui/hooks/__tests__/useDevDeploy.test.tsx | 33 +- src/cli/tui/screens/agent/types.ts | 1 + src/cli/tui/screens/generate/types.ts | 12 +- src/schema/__tests__/constants.test.ts | 13 +- src/schema/constants.ts | 21 +- 22 files changed, 665 insertions(+), 21 deletions(-) create mode 100644 src/assets/typescript/http/mastra/base/README.md create mode 100644 src/assets/typescript/http/mastra/base/gitignore.template create mode 100644 src/assets/typescript/http/mastra/base/main.ts create mode 100644 src/assets/typescript/http/mastra/base/model/load.ts create mode 100644 src/assets/typescript/http/mastra/base/package.json create mode 100644 src/assets/typescript/http/mastra/base/tsconfig.json create mode 100644 src/cli/templates/MastraRenderer.ts diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index bc6e49330..1bf46acbb 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -604,6 +604,12 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "python/mcp/standalone/base/main.py", "python/mcp/standalone/base/pyproject.toml", "typescript/.gitkeep", + "typescript/http/mastra/base/README.md", + "typescript/http/mastra/base/gitignore.template", + "typescript/http/mastra/base/main.ts", + "typescript/http/mastra/base/model/load.ts", + "typescript/http/mastra/base/package.json", + "typescript/http/mastra/base/tsconfig.json", "typescript/http/strands/base/README.md", "typescript/http/strands/base/gitignore.template", "typescript/http/strands/base/main.ts", @@ -5684,6 +5690,281 @@ When modifying JSON config files: exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/.gitkeep should match snapshot 1`] = `""`; +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/README.md should match snapshot 1`] = ` +"# {{name}} + +Mastra agent running on Amazon Bedrock AgentCore Runtime. + +## Development + +\`\`\`bash +npm install +npm run dev +\`\`\` + +## Build + +\`\`\`bash +npm run build +npm start +\`\`\` +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/gitignore.template should match snapshot 1`] = ` +"node_modules/ +dist/ +.env +.env.local +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/main.ts should match snapshot 1`] = ` +"import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { Agent } from '@mastra/core/agent'; +import { loadModel } from './model/load.js'; + +const SYSTEM_PROMPT = \`You are a helpful assistant.\`; + +let cachedAgent: Agent | null = null; + +async function getOrCreateAgent(): Promise { + if (!cachedAgent) { + const model = await loadModel(); + cachedAgent = new Agent({ + id: '{{name}}', + name: '{{name}}', + instructions: SYSTEM_PROMPT, + model, + }); + } + return cachedAgent; +} + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: any, context: any) { + const agent = await getOrCreateAgent(); + const stream = await agent.stream(payload.prompt ?? ''); + + for await (const chunk of stream.fullStream) { + if (chunk.type === 'text-delta') { + yield { data: chunk.payload.text }; + } + } + }, + }, +}); + +app.run({ port: parseInt(process.env.PORT ?? '8080') }); +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/model/load.ts should match snapshot 1`] = ` +"{{#if (eq modelProvider "Bedrock")}} +import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + +const provider = fromNodeProviderChain(); +const bedrockRegion = process.env.AWS_REGION ?? 'us-east-1'; + +const bedrock = createAmazonBedrock({ + region: bedrockRegion, + credentialProvider: async () => { + const creds = await provider(); + return { + accessKeyId: creds.accessKeyId, + secretAccessKey: creds.secretAccessKey, + sessionToken: creds.sessionToken, + }; + }, +}); + +function getDefaultBedrockModelId(region: string): string { + if (region === 'ap-northeast-1') { + return 'jp.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region.startsWith('eu-')) { + return 'eu.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region === 'ap-southeast-2') { + return 'au.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region.startsWith('us-')) { + return 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + return 'global.anthropic.claude-sonnet-4-5-20250929-v1:0'; +} + +export function loadModel() { + return bedrock(process.env.BEDROCK_MODEL_ID ?? getDefaultBedrockModelId(bedrockRegion)); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { createAnthropic } from '@ai-sdk/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _anthropic: ReturnType | undefined; + +async function getProvider() { + if (!_anthropic) { + const apiKey = await getApiKey(); + _anthropic = createAnthropic({ apiKey }); + } + return _anthropic; +} + +export async function loadModel() { + const anthropic = await getProvider(); + return anthropic('claude-sonnet-4-5-20250929'); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { createOpenAI } from '@ai-sdk/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _openai: ReturnType | undefined; + +async function getProvider() { + if (!_openai) { + const apiKey = await getApiKey(); + _openai = createOpenAI({ apiKey }); + } + return _openai; +} + +export async function loadModel() { + const openai = await getProvider(); + return openai('gpt-4.1'); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _google: ReturnType | undefined; + +async function getProvider() { + if (!_google) { + const apiKey = await getApiKey(); + _google = createGoogleGenerativeAI({ apiKey }); + } + return _google; +} + +export async function loadModel() { + const google = await getProvider(); + return google('gemini-2.5-flash'); +} +{{/if}} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/package.json should match snapshot 1`] = ` +"{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Mastra", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "@mastra/core": "^1.37.1", + "ai": "^6.0.0", + {{#if (eq modelProvider "Bedrock")}} + "@ai-sdk/amazon-bedrock": "^4.0.0", + "@aws-sdk/credential-providers": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Anthropic")}} + "@ai-sdk/anthropic": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "OpenAI")}} + "@ai-sdk/openai": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^3.0.0", + {{/if}} + "bedrock-agentcore": "^0.2.4", + "tsx": "^4.19.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/mastra/base/tsconfig.json should match snapshot 1`] = ` +"{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} +" +`; + exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/README.md should match snapshot 1`] = ` "This is a project generated by the AgentCore CLI! diff --git a/src/assets/typescript/http/mastra/base/README.md b/src/assets/typescript/http/mastra/base/README.md new file mode 100644 index 000000000..b699887d5 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/README.md @@ -0,0 +1,17 @@ +# {{name}} + +Mastra agent running on Amazon Bedrock AgentCore Runtime. + +## Development + +```bash +npm install +npm run dev +``` + +## Build + +```bash +npm run build +npm start +``` diff --git a/src/assets/typescript/http/mastra/base/gitignore.template b/src/assets/typescript/http/mastra/base/gitignore.template new file mode 100644 index 000000000..dafa69941 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/gitignore.template @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +.env.local diff --git a/src/assets/typescript/http/mastra/base/main.ts b/src/assets/typescript/http/mastra/base/main.ts new file mode 100644 index 000000000..0aab40165 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/main.ts @@ -0,0 +1,37 @@ +import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { Agent } from '@mastra/core/agent'; +import { loadModel } from './model/load.js'; + +const SYSTEM_PROMPT = `You are a helpful assistant.`; + +let cachedAgent: Agent | null = null; + +async function getOrCreateAgent(): Promise { + if (!cachedAgent) { + const model = await loadModel(); + cachedAgent = new Agent({ + id: '{{name}}', + name: '{{name}}', + instructions: SYSTEM_PROMPT, + model, + }); + } + return cachedAgent; +} + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: any, context: any) { + const agent = await getOrCreateAgent(); + const stream = await agent.stream(payload.prompt ?? ''); + + for await (const chunk of stream.fullStream) { + if (chunk.type === 'text-delta') { + yield { data: chunk.payload.text }; + } + } + }, + }, +}); + +app.run({ port: parseInt(process.env.PORT ?? '8080') }); diff --git a/src/assets/typescript/http/mastra/base/model/load.ts b/src/assets/typescript/http/mastra/base/model/load.ts new file mode 100644 index 000000000..865169945 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/model/load.ts @@ -0,0 +1,138 @@ +{{#if (eq modelProvider "Bedrock")}} +import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + +const provider = fromNodeProviderChain(); +const bedrockRegion = process.env.AWS_REGION ?? 'us-east-1'; + +const bedrock = createAmazonBedrock({ + region: bedrockRegion, + credentialProvider: async () => { + const creds = await provider(); + return { + accessKeyId: creds.accessKeyId, + secretAccessKey: creds.secretAccessKey, + sessionToken: creds.sessionToken, + }; + }, +}); + +function getDefaultBedrockModelId(region: string): string { + if (region === 'ap-northeast-1') { + return 'jp.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region.startsWith('eu-')) { + return 'eu.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region === 'ap-southeast-2') { + return 'au.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + if (region.startsWith('us-')) { + return 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'; + } + return 'global.anthropic.claude-sonnet-4-5-20250929-v1:0'; +} + +export function loadModel() { + return bedrock(process.env.BEDROCK_MODEL_ID ?? getDefaultBedrockModelId(bedrockRegion)); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { createAnthropic } from '@ai-sdk/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _anthropic: ReturnType | undefined; + +async function getProvider() { + if (!_anthropic) { + const apiKey = await getApiKey(); + _anthropic = createAnthropic({ apiKey }); + } + return _anthropic; +} + +export async function loadModel() { + const anthropic = await getProvider(); + return anthropic('claude-sonnet-4-5-20250929'); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { createOpenAI } from '@ai-sdk/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _openai: ReturnType | undefined; + +async function getProvider() { + if (!_openai) { + const apiKey = await getApiKey(); + _openai = createOpenAI({ apiKey }); + } + return _openai; +} + +export async function loadModel() { + const openai = await getProvider(); + return openai('gpt-4.1'); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); +} + +let _google: ReturnType | undefined; + +async function getProvider() { + if (!_google) { + const apiKey = await getApiKey(); + _google = createGoogleGenerativeAI({ apiKey }); + } + return _google; +} + +export async function loadModel() { + const google = await getProvider(); + return google('gemini-2.5-flash'); +} +{{/if}} diff --git a/src/assets/typescript/http/mastra/base/package.json b/src/assets/typescript/http/mastra/base/package.json new file mode 100644 index 000000000..0d74744a5 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/package.json @@ -0,0 +1,36 @@ +{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Mastra", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "@mastra/core": "^1.37.1", + "ai": "^6.0.0", + {{#if (eq modelProvider "Bedrock")}} + "@ai-sdk/amazon-bedrock": "^4.0.0", + "@aws-sdk/credential-providers": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Anthropic")}} + "@ai-sdk/anthropic": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "OpenAI")}} + "@ai-sdk/openai": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^3.0.0", + {{/if}} + "bedrock-agentcore": "^0.2.4", + "tsx": "^4.19.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/src/assets/typescript/http/mastra/base/tsconfig.json b/src/assets/typescript/http/mastra/base/tsconfig.json new file mode 100644 index 000000000..c199ae076 --- /dev/null +++ b/src/assets/typescript/http/mastra/base/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 070801f40..675478945 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -181,6 +181,14 @@ describe('validate', () => { }); expect(result.valid).toBe(true); + result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + language: 'TypeScript', + framework: 'Mastra', + modelProvider: 'Bedrock', + }); + expect(result.valid).toBe(true); + result = validateAddAgentOptions({ ...validAgentOptionsCreate, language: 'TypeScript', diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index cf339257c..5fb43feeb 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -17,6 +17,7 @@ import { TargetLanguageSchema, getSupportedFrameworksForProtocol, getSupportedModelProviders, + isTypeScriptSDKFramework, isValidKmsKeyArn, matchEnumValue, } from '../../../schema'; @@ -258,15 +259,10 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes if (options.language === 'Other') { return { valid: false, error: 'Create path only supports Python or TypeScript' }; } - if ( - options.language === 'TypeScript' && - options.framework && - options.framework !== 'Strands' && - options.framework !== 'VercelAI' - ) { + if (options.language === 'TypeScript' && options.framework && !isTypeScriptSDKFramework(options.framework)) { return { valid: false, - error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands, Vercel AI SDK, and Mastra are supported.`, }; } diff --git a/src/cli/commands/create/__tests__/validate.test.ts b/src/cli/commands/create/__tests__/validate.test.ts index ad1c999f7..1bed72add 100644 --- a/src/cli/commands/create/__tests__/validate.test.ts +++ b/src/cli/commands/create/__tests__/validate.test.ts @@ -133,6 +133,20 @@ describe('validateCreateOptions', () => { expect(result.valid).toBe(true); }); + it('accepts TypeScript with Mastra framework and Bedrock model provider', () => { + const result = validateCreateOptions( + { + name: 'TestProjMastra', + language: 'TypeScript', + framework: 'Mastra', + modelProvider: 'Bedrock', + memory: 'none', + }, + testDir + ); + expect(result.valid).toBe(true); + }); + it('rejects TypeScript with a non-Strands framework', () => { const result = validateCreateOptions( { diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index aa3f8e84e..1e703b7fe 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -361,7 +361,7 @@ export const registerCreate = (program: Command) => { .option('--language ', 'Target language: Python or TypeScript (default: Python) [non-interactive]') .option( '--framework ', - 'Agent framework (Strands, LangChain_LangGraph, GoogleADK, OpenAIAgents, VercelAI) [non-interactive]' + 'Agent framework (Strands, LangChain_LangGraph, GoogleADK, OpenAIAgents, VercelAI, Mastra) [non-interactive]' ) .option('--model-provider ', 'Model provider (Bedrock, Anthropic, OpenAI, Gemini) [non-interactive]') .option('--api-key ', 'API key for non-Bedrock providers [non-interactive]') diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index 9cc6964dd..e78eb8658 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -9,6 +9,7 @@ import { TargetLanguageSchema, getSupportedFrameworksForProtocol, getSupportedModelProviders, + isTypeScriptSDKFramework, matchEnumValue, } from '../../../schema'; import type { ProtocolMode } from '../../../schema'; @@ -192,11 +193,10 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val return { valid: false, error: `Invalid model provider: ${options.modelProvider}` }; } - // TypeScript supports Strands and Vercel AI only - if (options.language === 'TypeScript' && fwResult.data !== 'Strands' && fwResult.data !== 'VercelAI') { + if (options.language === 'TypeScript' && !isTypeScriptSDKFramework(fwResult.data)) { return { valid: false, - error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands, Vercel AI SDK, and Mastra are supported.`, }; } diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index f6b987428..ab042a888 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -246,7 +246,7 @@ export class AgentPrimitive extends BasePrimitive', 'Language: Python (create), or Python/TypeScript/Other (BYO) [non-interactive]') .option( '--framework ', - 'Framework: Strands, LangChain_LangGraph, GoogleADK, OpenAIAgents, VercelAI [non-interactive]' + 'Framework: Strands, LangChain_LangGraph, GoogleADK, OpenAIAgents, VercelAI, Mastra [non-interactive]' ) .option('--model-provider ', 'Model provider: Bedrock, Anthropic, OpenAI, Gemini [non-interactive]') .option('--api-key ', 'API key for non-Bedrock providers [non-interactive]') diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 475b0100b..3e103100f 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -73,7 +73,14 @@ export const FilterType = z.enum([ 'none', ]); export const AgentEnvironment = z.enum(['harness', 'runtime']); -export const AgentFramework = z.enum(['strands', 'langchain_langgraph', 'googleadk', 'openaiagents']); +export const AgentFramework = z.enum([ + 'strands', + 'langchain_langgraph', + 'googleadk', + 'openaiagents', + 'vercelai', + 'mastra', +]); export const GatewayTargetHost = z.enum(['lambda', 'agentcoreruntime']); export const GatewayTargetType = z.enum([ 'mcp-server', diff --git a/src/cli/templates/MastraRenderer.ts b/src/cli/templates/MastraRenderer.ts new file mode 100644 index 000000000..96dc14ded --- /dev/null +++ b/src/cli/templates/MastraRenderer.ts @@ -0,0 +1,9 @@ +import { BaseRenderer } from './BaseRenderer'; +import { TEMPLATE_ROOT } from './templateRoot'; +import type { AgentRenderConfig } from './types'; + +export class MastraRenderer extends BaseRenderer { + constructor(config: AgentRenderConfig) { + super(config, 'mastra', TEMPLATE_ROOT, config.protocol ?? 'http'); + } +} diff --git a/src/cli/templates/__tests__/BaseRenderer.test.ts b/src/cli/templates/__tests__/BaseRenderer.test.ts index ebac2150f..444e67df4 100644 --- a/src/cli/templates/__tests__/BaseRenderer.test.ts +++ b/src/cli/templates/__tests__/BaseRenderer.test.ts @@ -52,6 +52,16 @@ describe('BaseRenderer', () => { expect(renderer.getTemplateDirPublic()).toBe('/templates/python/a2a/strands'); }); + it('getTemplateDir supports Mastra TypeScript HTTP templates', () => { + const renderer = new TestRenderer( + { targetLanguage: 'TypeScript', name: 'MyAgent', hasMemory: false, protocol: 'HTTP' }, + 'mastra', + '/templates' + ); + + expect(renderer.getTemplateDirPublic()).toBe('/templates/typescript/http/mastra'); + }); + it('getTemplateDir uses explicit protocol over config', () => { const renderer = new TestRenderer( { targetLanguage: 'Python', name: 'MyAgent', hasMemory: false, protocol: 'A2A' }, diff --git a/src/cli/templates/index.ts b/src/cli/templates/index.ts index e41e563b3..966441914 100644 --- a/src/cli/templates/index.ts +++ b/src/cli/templates/index.ts @@ -1,6 +1,7 @@ import type { BaseRenderer } from './BaseRenderer'; import { GoogleADKRenderer } from './GoogleADKRenderer'; import { LangGraphRenderer } from './LangGraphRenderer'; +import { MastraRenderer } from './MastraRenderer'; import { McpRenderer } from './McpRenderer'; import { OpenAIAgentsRenderer } from './OpenAIAgentsRenderer'; import { StrandsRenderer } from './StrandsRenderer'; @@ -12,6 +13,7 @@ export { CDKRenderer, type CDKRendererContext } from './CDKRenderer'; export { renderGatewayTargetTemplate } from './GatewayTargetRenderer'; export { GoogleADKRenderer } from './GoogleADKRenderer'; export { LangGraphRenderer } from './LangGraphRenderer'; +export { MastraRenderer } from './MastraRenderer'; export { McpRenderer } from './McpRenderer'; export { OpenAIAgentsRenderer } from './OpenAIAgentsRenderer'; export { StrandsRenderer } from './StrandsRenderer'; @@ -38,6 +40,8 @@ export function createRenderer(config: AgentRenderConfig): BaseRenderer { return new OpenAIAgentsRenderer(config); case 'VercelAI': return new VercelAIRenderer(config); + case 'Mastra': + return new MastraRenderer(config); default: { const _exhaustive: never = config.sdkFramework; throw new Error(`Unsupported SDK framework: ${String(_exhaustive)}`); diff --git a/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx b/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx index de56d0e29..bc9949d1f 100644 --- a/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx +++ b/src/cli/tui/hooks/__tests__/useDevDeploy.test.tsx @@ -1,9 +1,27 @@ import { useDevDeploy } from '../useDevDeploy.js'; import { Text } from 'ink'; import { render } from 'ink-testing-library'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockCanSkipDeploy, mockConfigIOInstance, mockHandleDeploy } = vi.hoisted(() => ({ + mockCanSkipDeploy: vi.fn(), + mockConfigIOInstance: { + readAWSDeploymentTargets: vi.fn(), + readProjectSpec: vi.fn(), + writeAWSDeploymentTargets: vi.fn(), + }, + mockHandleDeploy: vi.fn(), +})); + +vi.mock('../../../../lib/index.js', () => ({ + ConfigIO: vi.fn(function () { + return mockConfigIOInstance; + }), +})); -const mockHandleDeploy = vi.fn(); +vi.mock('../../../operations/deploy/change-detection.js', () => ({ + canSkipDeploy: mockCanSkipDeploy, +})); vi.mock('../../../commands/deploy/actions.js', () => ({ handleDeploy: (...args: unknown[]) => mockHandleDeploy(...args), @@ -23,6 +41,17 @@ describe('useDevDeploy', () => { vi.clearAllMocks(); }); + beforeEach(() => { + mockCanSkipDeploy.mockResolvedValue(false); + mockConfigIOInstance.readAWSDeploymentTargets.mockResolvedValue([ + { name: 'default', account: '123456789012', region: 'us-east-1' }, + ]); + mockConfigIOInstance.readProjectSpec.mockResolvedValue({ + harnesses: [{ name: 'test-harness', path: 'app/test-harness' }], + runtimes: [], + }); + }); + it('calls handleDeploy on mount', async () => { mockHandleDeploy.mockResolvedValue({ success: true }); diff --git a/src/cli/tui/screens/agent/types.ts b/src/cli/tui/screens/agent/types.ts index f4ad9ae27..37c0d1f55 100644 --- a/src/cli/tui/screens/agent/types.ts +++ b/src/cli/tui/screens/agent/types.ts @@ -165,6 +165,7 @@ export const FRAMEWORK_OPTIONS = [ { id: 'LangChain_LangGraph', title: 'LangChain + LangGraph', description: 'Popular open-source frameworks' }, { id: 'GoogleADK', title: 'Google ADK', description: 'Google Agent Development Kit' }, { id: 'OpenAIAgents', title: 'OpenAI Agents', description: 'OpenAI native agent SDK' }, + { id: 'Mastra', title: 'Mastra', description: 'TypeScript agent framework' }, ] as const; export const MODEL_PROVIDER_OPTIONS = [ diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 2f9a304fd..24b207bb2 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -7,7 +7,12 @@ import type { SDKFramework, TargetLanguage, } from '../../../../schema'; -import { DEFAULT_MODEL_IDS, PROTOCOL_FRAMEWORK_MATRIX, getSupportedModelProviders } from '../../../../schema'; +import { + DEFAULT_MODEL_IDS, + PROTOCOL_FRAMEWORK_MATRIX, + getSupportedModelProviders, + isTypeScriptSDKFramework, +} from '../../../../schema'; import type { JwtConfigOptions } from '../../../primitives/auth-utils'; export type GenerateStep = @@ -138,17 +143,18 @@ export const SDK_OPTIONS = [ { id: 'GoogleADK', title: 'Google ADK', description: 'Google Agent Development Kit' }, { id: 'OpenAIAgents', title: 'OpenAI Agents', description: 'OpenAI native agent SDK' }, { id: 'VercelAI', title: 'Vercel AI SDK', description: 'Vercel AI SDK for TypeScript agents' }, + { id: 'Mastra', title: 'Mastra', description: 'TypeScript agent framework' }, ] as const; /** * Get SDK options filtered by protocol compatibility and target language. - * TypeScript currently only supports Strands. + * TypeScript currently supports a subset of HTTP SDK frameworks. */ export function getSDKOptionsForProtocol(protocol: ProtocolMode, language?: TargetLanguage) { const supportedFrameworks = PROTOCOL_FRAMEWORK_MATRIX[protocol]; const byProtocol = SDK_OPTIONS.filter(option => supportedFrameworks.includes(option.id)); if (language === 'TypeScript') { - return byProtocol.filter(option => option.id === 'Strands' || option.id === 'VercelAI'); + return byProtocol.filter(option => isTypeScriptSDKFramework(option.id)); } return byProtocol; } diff --git a/src/schema/__tests__/constants.test.ts b/src/schema/__tests__/constants.test.ts index 2d8bd3bc9..39cbdef88 100644 --- a/src/schema/__tests__/constants.test.ts +++ b/src/schema/__tests__/constants.test.ts @@ -35,6 +35,7 @@ describe('matchEnumValue', () => { expect(matchEnumValue(SDKFrameworkSchema, 'langchain_langgraph')).toBe('LangChain_LangGraph'); expect(matchEnumValue(SDKFrameworkSchema, 'openaiagents')).toBe('OpenAIAgents'); expect(matchEnumValue(SDKFrameworkSchema, 'googleadk')).toBe('GoogleADK'); + expect(matchEnumValue(SDKFrameworkSchema, 'mastra')).toBe('Mastra'); }); }); @@ -42,6 +43,7 @@ describe('SDKFrameworkSchema', () => { it('accepts valid frameworks and rejects invalid', () => { expect(SDKFrameworkSchema.safeParse('Strands').success).toBe(true); expect(SDKFrameworkSchema.safeParse('OpenAIAgents').success).toBe(true); + expect(SDKFrameworkSchema.safeParse('Mastra').success).toBe(true); expect(SDKFrameworkSchema.safeParse('AutoGen').success).toBe(false); expect(SDKFrameworkSchema.safeParse('strands').success).toBe(false); // case-sensitive }); @@ -94,6 +96,10 @@ describe('getSupportedModelProviders', () => { it('returns only OpenAI for OpenAIAgents', () => { expect(getSupportedModelProviders('OpenAIAgents')).toEqual(['OpenAI']); }); + + it('returns all 4 providers for Mastra', () => { + expect(getSupportedModelProviders('Mastra')).toEqual(['Bedrock', 'Anthropic', 'OpenAI', 'Gemini']); + }); }); describe('isModelProviderSupported', () => { @@ -142,7 +148,7 @@ describe('PROTOCOL_FRAMEWORK_MATRIX', () => { it('HTTP supports all visible frameworks', () => { expect(PROTOCOL_FRAMEWORK_MATRIX.HTTP).toEqual( - expect.arrayContaining(['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents']) + expect.arrayContaining(['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'Mastra']) ); }); @@ -188,6 +194,11 @@ describe('isFrameworkSupportedForProtocol', () => { expect(isFrameworkSupportedForProtocol('A2A', 'OpenAIAgents')).toBe(false); }); + it('returns true for Mastra + HTTP and false for Mastra + A2A', () => { + expect(isFrameworkSupportedForProtocol('HTTP', 'Mastra')).toBe(true); + expect(isFrameworkSupportedForProtocol('A2A', 'Mastra')).toBe(false); + }); + it('returns false for any framework + MCP', () => { expect(isFrameworkSupportedForProtocol('MCP', 'Strands')).toBe(false); expect(isFrameworkSupportedForProtocol('MCP', 'OpenAIAgents')).toBe(false); diff --git a/src/schema/constants.ts b/src/schema/constants.ts index ca8732b45..0cc3d08ae 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -4,9 +4,23 @@ import { z } from 'zod'; // Feature Constants (shared across all schemas) // ============================================================================ -export const SDKFrameworkSchema = z.enum(['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'VercelAI']); +export const SDKFrameworkSchema = z.enum([ + 'Strands', + 'LangChain_LangGraph', + 'GoogleADK', + 'OpenAIAgents', + 'VercelAI', + 'Mastra', +]); export type SDKFramework = z.infer; +export const TYPESCRIPT_SDK_FRAMEWORKS = ['Strands', 'VercelAI', 'Mastra'] as const satisfies readonly SDKFramework[]; +export type TypeScriptSDKFramework = (typeof TYPESCRIPT_SDK_FRAMEWORKS)[number]; + +export function isTypeScriptSDKFramework(framework: SDKFramework): framework is TypeScriptSDKFramework { + return TYPESCRIPT_SDK_FRAMEWORKS.includes(framework as TypeScriptSDKFramework); +} + export const TargetLanguageSchema = z.enum(['Python', 'TypeScript', 'Other']); export type TargetLanguage = z.infer; @@ -48,6 +62,7 @@ export const SDK_MODEL_PROVIDER_MATRIX: Record; * MCP is a standalone tool server with no framework. */ export const PROTOCOL_FRAMEWORK_MATRIX: Record = { - HTTP: ['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'VercelAI'] as const, + HTTP: ['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'VercelAI', 'Mastra'] as const, MCP: [] as const, A2A: ['Strands', 'GoogleADK', 'LangChain_LangGraph'] as const, AGUI: ['Strands', 'LangChain_LangGraph', 'GoogleADK'] as const,