@chinchillaenterprises/mcp-amplify
Version:
AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery
895 lines • 40.6 kB
JavaScript
import { ListAppsCommand, GetAppCommand, ListBranchesCommand, ListJobsCommand, GetJobCommand, CreateAppCommand, CreateBranchCommand, StartJobCommand, UpdateAppCommand } from "@aws-sdk/client-amplify";
import { ListRolesCommand } from "@aws-sdk/client-iam";
import { getCurrentClients } from './account-handlers.js';
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
// Default build spec template with Node 20+ and CDK support
const DEFAULT_BUILD_SPEC = `version: 1
backend:
phases:
preBuild:
commands:
- nvm use 20
build:
commands:
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
preBuild:
commands:
- nvm use 20
- npm ci --cache .npm --prefer-offline
build:
commands:
- env | grep -E '^NEXT_PUBLIC_' >> .env.production
- env | grep -E '^AMPLIFY_' >> .env.production
- npx ampx generate outputs --branch $AWS_BRANCH --app-id $AWS_APP_ID
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- .npm/**/*
- node_modules/**/*`;
export async function handleAmplifyListApps(args) {
const { maxResults = 25 } = args;
try {
const clients = getCurrentClients();
const command = new ListAppsCommand({
maxResults: Math.min(maxResults, 100)
});
const response = await clients.amplify.send(command);
return {
apps: response.apps || [],
totalApps: response.apps?.length || 0,
nextToken: response.nextToken || null
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to list Amplify apps: ${errorMessage}`);
}
}
export async function handleAmplifyGetAppInfo(args) {
const { appId } = args;
if (!appId) {
throw new Error('appId is required');
}
try {
const clients = getCurrentClients();
// Get app details
const getAppCommand = new GetAppCommand({ appId });
const appResponse = await clients.amplify.send(getAppCommand);
if (!appResponse.app) {
throw new Error(`App ${appId} not found`);
}
// Get branches
const listBranchesCommand = new ListBranchesCommand({
appId,
maxResults: 50
});
const branchesResponse = await clients.amplify.send(listBranchesCommand);
// Enhance branch info with additional details
const enhancedBranches = (branchesResponse.branches || []).map((branch) => ({
...branch,
resourceNamingInfo: {
expectedLogGroups: [
`/aws/lambda/${appResponse.app.name}-${branch.branchName}-*`,
`/aws/appsync/apis/${appResponse.app.name}-${branch.branchName}-*`,
`/aws/cognito/userpools/${appResponse.app.name}-${branch.branchName}-*`
],
stackNamePattern: `amplify-${appResponse.app.name}-${branch.branchName}-*`,
resourcePrefix: `${appResponse.app.name}-${branch.branchName}`
}
}));
return {
...appResponse.app,
branches: enhancedBranches,
totalBranches: enhancedBranches.length,
activeBranches: enhancedBranches.filter((b) => b.enableAutoBuild).length,
cloudWatchLogInfo: {
appLogGroupPatterns: [
`/aws/lambda/${appResponse.app.name}-*`,
`/aws/appsync/apis/*${appResponse.app.name}*`,
`/aws/cognito/userpools/*${appResponse.app.name}*`
],
searchPatterns: enhancedBranches.map((branch) => ({
branchName: branch.branchName,
patterns: [
`/aws/lambda/${appResponse.app.name}-${branch.branchName}-*`,
`/aws/appsync/apis/${appResponse.app.name}-${branch.branchName}-*`
]
}))
}
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get app info: ${errorMessage}`);
}
}
export async function handleAmplifyListBranches(args) {
const { appId, maxResults = 50 } = args;
if (!appId) {
throw new Error('appId is required');
}
try {
const clients = getCurrentClients();
const command = new ListBranchesCommand({
appId,
maxResults: Math.min(maxResults, 100)
});
const response = await clients.amplify.send(command);
return {
appId,
branches: response.branches || [],
totalBranches: response.branches?.length || 0,
nextToken: response.nextToken || null
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to list branches: ${errorMessage}`);
}
}
export async function handleAmplifyGetBuildLogs(args) {
const { appId, branchName, jobId, maxLines = 1000 } = args;
if (!appId || !branchName) {
throw new Error('appId and branchName are required');
}
try {
const clients = getCurrentClients();
let targetJobId = jobId;
// If no jobId provided, get the latest build job
if (!targetJobId) {
const listJobsCommand = new ListJobsCommand({
appId,
branchName,
maxResults: 1
});
const jobsResponse = await clients.amplify.send(listJobsCommand);
if (!jobsResponse.jobSummaries || jobsResponse.jobSummaries.length === 0) {
return {
appId,
branchName,
error: "No build jobs found for this branch",
logs: []
};
}
targetJobId = jobsResponse.jobSummaries[0].jobId;
}
// Get the job details including logs
const getJobCommand = new GetJobCommand({
appId,
branchName,
jobId: targetJobId
});
const jobResponse = await clients.amplify.send(getJobCommand);
if (!jobResponse.job) {
throw new Error(`Job ${targetJobId} not found`);
}
// Format the response
const logs = jobResponse.job.steps?.flatMap((step) => step.logUrl ? [{
stepName: step.stepName,
status: step.status,
startTime: step.startTime,
endTime: step.endTime,
duration: step.endTime && step.startTime ?
new Date(step.endTime).getTime() - new Date(step.startTime).getTime() : undefined,
logUrl: step.logUrl,
screenshots: step.screenshots
}] : []) || [];
return {
appId,
branchName,
jobId: targetJobId,
jobType: jobResponse.job.jobType,
status: jobResponse.job.summary?.status,
commitId: jobResponse.job.summary?.commitId,
commitMessage: jobResponse.job.summary?.commitMessage,
startTime: jobResponse.job.summary?.startTime,
endTime: jobResponse.job.summary?.endTime,
duration: jobResponse.job.summary?.duration,
logs,
totalSteps: logs.length,
logMessage: "Use the logUrl for each step to view detailed logs in the AWS Console"
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to get build logs: ${errorMessage}`);
}
}
export async function handleAmplifyCreateNextjsApp(args) {
const { projectName, directory = process.cwd(), installDependencies = false, initGit = false, useCurrentDirectory = false } = args;
if (!projectName) {
throw new Error('projectName is required');
}
// Handle current directory case
const isCurrentDirectory = useCurrentDirectory || projectName === '.';
// Validate project name (skip validation for current directory)
if (!isCurrentDirectory && !/^[a-zA-Z0-9-_]+$/.test(projectName)) {
throw new Error('Project name can only contain letters, numbers, hyphens, and underscores');
}
const projectPath = isCurrentDirectory ? directory : path.join(directory, projectName);
// Safety check: ensure we're not accidentally working with parent directories
if (!isCurrentDirectory && projectPath === directory) {
throw new Error('Invalid project configuration - would operate on parent directory');
}
// Additional safety: ensure projectPath doesn't contain dangerous patterns
if (projectPath.includes('..') || projectPath === '/' || projectPath === os.homedir()) {
throw new Error('Invalid project path - contains dangerous patterns');
}
try {
// Check if directory already exists (only for new directories)
if (!isCurrentDirectory) {
try {
await fs.access(projectPath);
throw new Error(`Directory ${projectPath} already exists`);
}
catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
else {
// For current directory, check if it's empty (excluding .git and common ignore files)
const files = await fs.readdir(projectPath);
const ignoredFiles = ['.git', '.gitignore', '.DS_Store'];
const nonIgnoredFiles = files.filter(f => !ignoredFiles.includes(f));
if (nonIgnoredFiles.length > 0) {
throw new Error(`Current directory is not empty. Please use an empty directory. Found: ${nonIgnoredFiles.join(', ')}`);
}
}
// Clone the Chill Amplify Template repository
console.error(`Cloning Chill Amplify Template to ${projectPath}...`);
if (isCurrentDirectory) {
// Clone into current directory using temporary folder
const tempDir = path.join(directory, '.amplify-template-temp');
try {
// Ensure temp directory doesn't exist from previous failed attempts
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
await execCommand('git', ['clone', 'https://github.com/ChinchillaEnterprises/chill-amplify-template.git', tempDir]);
// Move contents from temp to current directory
const tempFiles = await fs.readdir(tempDir);
for (const file of tempFiles) {
// Skip .git directory from template
if (file === '.git') {
continue;
}
const sourcePath = path.join(tempDir, file);
const targetPath = path.join(projectPath, file);
// Check if target already exists
try {
await fs.access(targetPath);
// If it exists and it's .gitignore, we should merge or skip
if (file === '.gitignore') {
console.error(`Skipping ${file} as it already exists`);
continue;
}
}
catch (e) {
// File doesn't exist, we can move it
}
await fs.rename(sourcePath, targetPath);
}
// Remove temp directory
await fs.rm(tempDir, { recursive: true, force: true });
}
catch (error) {
// Clean up temp directory on error
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
throw error;
}
}
else {
await execCommand('git', ['clone', 'https://github.com/ChinchillaEnterprises/chill-amplify-template.git', projectPath]);
}
// Delete the .git folder from the cloned template to remove the upstream connection
console.error('Removing template git history...');
await fs.rm(path.join(projectPath, '.git'), { recursive: true, force: true });
// Update package.json name to match the project name
if (!isCurrentDirectory) {
console.error('Updating package.json name...');
const packageJsonPath = path.join(projectPath, 'package.json');
try {
// Read the existing package.json
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
// Update the name field to match the project name (convert to lowercase for npm compatibility)
packageJson.name = projectName.toLowerCase();
// Write the updated package.json back with proper formatting
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
console.error(`Updated package.json name to: ${projectName}`);
}
catch (error) {
// Log the error but don't fail the entire operation
console.error(`Warning: Could not update package.json name: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Handle git initialization
if (isCurrentDirectory) {
// For current directory, check if git is already initialized
let hasGit = false;
try {
await fs.access(path.join(projectPath, '.git'));
hasGit = true;
}
catch (e) {
// No git directory
}
if (!hasGit && initGit) {
console.error('Initializing git repository...');
await execCommand('git', ['init'], { cwd: projectPath });
}
// Only add and commit if we have git (either existing or just initialized)
if (hasGit || initGit) {
console.error('Adding Amplify template files to git...');
await execCommand('git', ['add', '-A'], { cwd: projectPath });
try {
await execCommand('git', ['commit', '-m', 'Add Chill Amplify Template'], { cwd: projectPath });
}
catch (e) {
// Might fail if there are no changes or user hasn't set git config
console.error('Note: Git commit skipped (no changes or git not configured)');
}
}
}
else {
// For new directory, handle git initialization
if (initGit) {
console.error('Initializing new git repository...');
await execCommand('git', ['init'], { cwd: projectPath });
// Create initial commit
await execCommand('git', ['add', '-A'], { cwd: projectPath });
await execCommand('git', ['commit', '-m', 'Initial commit from Chill Amplify Template'], { cwd: projectPath });
}
}
// Install dependencies if requested
if (installDependencies) {
console.error('Installing npm dependencies (this may take a few minutes)...');
await execCommand('npm', ['install'], { cwd: projectPath });
}
// Read package.json to get project info
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
return {
success: true,
projectName: isCurrentDirectory ? path.basename(projectPath) : projectName,
projectPath,
templateVersion: packageJson.version || '1.0.0',
dependencies: {
installed: installDependencies,
count: Object.keys(packageJson.dependencies || {}).length,
devCount: Object.keys(packageJson.devDependencies || {}).length
},
git: {
initialized: initGit,
remote: null
},
nextSteps: [
!isCurrentDirectory ? `cd ${projectName}` : null,
!installDependencies ? 'npm install' : null,
'npx ampx sandbox',
'npm run dev',
'Deploy to AWS Amplify Console when ready'
].filter(Boolean),
structure: {
hasAmplifyFolder: true,
hasAuth: true,
hasData: true,
hasSrcFolder: true,
isTypeScript: true,
isAppRouter: true
},
message: `✅ Successfully created Amplify Next.js app using Chill Amplify Template: ${isCurrentDirectory ? path.basename(projectPath) : projectName}\n\n` +
`The project includes (with latest Amplify v1.16.1+):\n` +
`- Pre-configured Amplify backend (auth, data)\n` +
`- TypeScript setup\n` +
`- Next.js App Router\n` +
`- Sample todo application\n` +
`- Latest package versions (no manual updates needed)\n\n` +
`To get started:\n` +
(isCurrentDirectory ? '' : `1. cd ${projectName}\n`) +
`${isCurrentDirectory ? '1' : '2'}. Set up AWS credentials (if not already done)\n` +
`${isCurrentDirectory ? '2' : '3'}. Run 'npx ampx sandbox' to deploy a cloud sandbox\n` +
`${isCurrentDirectory ? '3' : '4'}. Run 'npm run dev' to start the development server`
};
}
catch (error) {
// NO CLEANUP - if the operation fails, we leave any created files/directories intact
// This is safer than risking deletion of important data
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`\nError occurred. Any created files have been left in place at: ${projectPath}`);
console.error(`You may want to manually clean up before trying again.`);
throw new Error(`Failed to create Next.js app: ${errorMessage}`);
}
}
// Helper function to execute shell commands
function execCommand(command, args, options) {
return new Promise((resolve, reject) => {
const proc = spawn(command, args, {
stdio: 'pipe',
...options
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('error', (error) => {
reject(error);
});
proc.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Command '${command} ${args.join(' ')}' failed with code ${code}: ${stderr}`));
}
else {
resolve();
}
});
});
}
export async function handleAmplifyDeployApp(args) {
const { appName, githubOwner, githubRepo, branch = 'main', platform = 'WEB_COMPUTE', environmentVariables = {}, enableBranchAutoBuild = true, enableBranchAutoDeletion = false } = args;
if (!appName || !githubOwner || !githubRepo) {
throw new Error('appName, githubOwner, and githubRepo are required');
}
try {
const clients = getCurrentClients();
const accountState = await import('./account-handlers.js').then(m => m.getAccountState());
const activeAccount = accountState.accounts.find(acc => acc.id === accountState.activeAccountId);
if (!activeAccount?.githubToken) {
throw new Error('GitHub token is required. Please add an account with GitHub credentials or update the active account.');
}
// Validate that the GitHub repository exists and is accessible
const { Octokit } = await import('@octokit/rest');
const octokit = new Octokit({
auth: activeAccount.githubToken
});
try {
await octokit.repos.get({
owner: githubOwner,
repo: githubRepo
});
}
catch (error) {
if (error.status === 404) {
throw new Error(`GitHub repository ${githubOwner}/${githubRepo} not found or not accessible with the provided token`);
}
throw new Error(`Failed to validate GitHub repository: ${error.message}`);
}
// STEP 1: Check if app already exists for this repository
console.error(`Checking if app already exists for ${githubOwner}/${githubRepo}...`);
const existingAppCheck = await handleAmplifyCheckAppExists({ githubOwner, githubRepo });
if (existingAppCheck.exists) {
console.error(`Found existing app: ${existingAppCheck.app.name} (${existingAppCheck.app.appId})`);
const existingApp = existingAppCheck.app;
const appId = existingApp.appId;
const appUrl = `https://${branch}.${appId}.amplifyapp.com`;
// Check if the target branch exists
const targetBranch = existingApp.branches.find((b) => b.branchName === branch);
if (!targetBranch) {
// Create the missing branch
console.error(`Creating missing branch: ${branch}...`);
const createBranchCommand = new CreateBranchCommand({
appId,
branchName: branch,
stage: branch === 'main' || branch === 'master' ? 'PRODUCTION' : 'DEVELOPMENT',
enableAutoBuild: enableBranchAutoBuild,
enableNotification: false,
framework: 'Next.js - SSR',
environmentVariables: environmentVariables
});
await clients.amplify.send(createBranchCommand);
}
// STEP 2: Update build spec with Node 20+ defaults if needed
console.error(`Updating build spec to Node 20+ configuration...`);
try {
await handleAmplifyUpdateBuildSpec({ appId });
console.error(`✅ Build spec updated successfully`);
}
catch (buildSpecError) {
console.error(`⚠️ Build spec update failed: ${buildSpecError instanceof Error ? buildSpecError.message : String(buildSpecError)}`);
}
// STEP 3: Setup IAM role if needed
console.error(`Checking and setting up IAM service role...`);
try {
const iamResult = await handleAmplifySetupIamRole({ appId });
if (iamResult.success) {
console.error(`✅ IAM role assigned: ${iamResult.iamServiceRoleArn}`);
}
else {
console.error(`⚠️ IAM role setup skipped: ${iamResult.error}`);
}
}
catch (iamError) {
console.error(`⚠️ IAM role setup failed: ${iamError instanceof Error ? iamError.message : String(iamError)}`);
}
// STEP 4: Trigger deployment if never deployed or force deploy
const hasDeployments = existingApp.hasDeployments;
if (!hasDeployments || !targetBranch?.totalNumberOfJobs) {
console.error(`Triggering initial deployment for branch: ${branch}...`);
try {
const deploymentResult = await handleAmplifyTriggerDeployment({ appId, branchName: branch });
return {
success: true,
appId,
appName: existingApp.name,
appUrl,
repository: `${githubOwner}/${githubRepo}`,
branch,
platform: existingApp.platform,
existingApp: true,
updatesApplied: {
buildSpecUpdated: true,
iamRoleConfigured: true,
deploymentTriggered: true
},
deployment: deploymentResult.deployment,
nextSteps: [
`Monitor deployment progress at: https://console.aws.amazon.com/amplify/home#/${appId}/${branch}`,
`View app (when ready): ${appUrl}`,
'Run "amplify_get_build_logs" to check deployment status'
],
message: `✅ Successfully configured and deployed existing Amplify app: ${existingApp.name}\n\n` +
`The app has been updated with Node 20+ configuration and deployment triggered.\n` +
`Monitor the progress in the AWS Amplify console.`
};
}
catch (deployError) {
return {
success: false,
appId,
appName: existingApp.name,
existingApp: true,
error: `Failed to trigger deployment: ${deployError instanceof Error ? deployError.message : String(deployError)}`,
suggestion: `Use amplify_trigger_deployment manually or check the AWS Amplify console.`
};
}
}
else {
// App exists and has deployments
return {
success: true,
appId,
appName: existingApp.name,
appUrl,
repository: `${githubOwner}/${githubRepo}`,
branch,
platform: existingApp.platform,
existingApp: true,
hasDeployments: true,
updatesApplied: {
buildSpecUpdated: true,
iamRoleConfigured: true,
deploymentTriggered: false
},
branches: existingApp.branches,
nextSteps: [
`View app: ${appUrl}`,
'Use "amplify_trigger_deployment" to manually trigger a new deployment',
'Use "amplify_get_build_logs" to check recent deployment status'
],
message: `✅ Found existing Amplify app: ${existingApp.name}\n\n` +
`The app is already configured and deployed. Configuration has been updated to Node 20+.\n` +
`Use manual deployment trigger if you want to redeploy.`
};
}
}
// STEP 5: Create new app if none exists
console.error(`Creating new Amplify app: ${appName}...`);
// Create the Amplify app with updated build spec
const createAppCommand = new CreateAppCommand({
name: appName,
platform: platform,
repository: `https://github.com/${githubOwner}/${githubRepo}`,
oauthToken: activeAccount.githubToken,
environmentVariables: environmentVariables,
enableBranchAutoBuild: enableBranchAutoBuild,
enableBranchAutoDeletion: enableBranchAutoDeletion,
enableAutoBranchCreation: false,
buildSpec: DEFAULT_BUILD_SPEC // Use the new Node 20+ build spec
});
let appId;
let appUrl;
try {
const createAppResponse = await clients.amplify.send(createAppCommand);
if (!createAppResponse.app) {
throw new Error('Failed to create app - no app returned');
}
appId = createAppResponse.app.appId;
appUrl = `https://${branch}.${appId}.amplifyapp.com`;
console.error(`App created with ID: ${appId}`);
}
catch (error) {
if (error.message?.includes('already exists') || error.message?.includes('already connected')) {
throw new Error(`An Amplify app is already connected to repository ${githubOwner}/${githubRepo}. Use amplify_check_app_exists to find the existing app.`);
}
throw error;
}
// Setup IAM role for new app
console.error(`Setting up IAM service role for new app...`);
try {
const iamResult = await handleAmplifySetupIamRole({ appId });
if (iamResult.success) {
console.error(`✅ IAM role assigned: ${iamResult.iamServiceRoleArn}`);
}
}
catch (iamError) {
console.error(`⚠️ IAM role setup failed: ${iamError instanceof Error ? iamError.message : String(iamError)}`);
}
// Create the branch
console.error(`Creating branch: ${branch}...`);
const createBranchCommand = new CreateBranchCommand({
appId,
branchName: branch,
stage: branch === 'main' || branch === 'master' ? 'PRODUCTION' : 'DEVELOPMENT',
enableAutoBuild: enableBranchAutoBuild,
enableNotification: false,
framework: 'Next.js - SSR',
environmentVariables: environmentVariables
});
await clients.amplify.send(createBranchCommand);
// Start the deployment using StartJobCommand for initial build
console.error(`Starting initial deployment job...`);
const startJobCommand = new StartJobCommand({
appId,
branchName: branch,
jobType: 'RELEASE'
});
const deploymentResponse = await clients.amplify.send(startJobCommand);
return {
success: true,
appId,
appName,
appUrl,
repository: `${githubOwner}/${githubRepo}`,
branch,
platform,
existingApp: false,
configuredFeatures: {
node20BuildSpec: true,
iamRoleAssigned: true,
cdkSupport: true
},
environmentVariables: Object.keys(environmentVariables).length > 0 ? Object.keys(environmentVariables) : null,
deployment: {
jobId: deploymentResponse.jobSummary?.jobId,
status: deploymentResponse.jobSummary?.status || 'PENDING',
message: 'Deployment started. It may take 5-10 minutes to complete.'
},
nextSteps: [
`Monitor deployment progress at: https://console.aws.amazon.com/amplify/home#/${appId}/${branch}`,
`View app (when ready): ${appUrl}`,
'Run "amplify_get_build_logs" to check deployment status',
'Set up custom domain if needed'
],
message: `✅ Successfully created and deployed Amplify app: ${appName}\n\n` +
`The app has been configured with Node 20+ build spec and IAM role.\n` +
`Deployment is now in progress. This typically takes 5-10 minutes.`
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to deploy Amplify app: ${errorMessage}`);
}
}
export async function handleAmplifyCheckAppExists(args) {
const { githubOwner, githubRepo } = args;
if (!githubOwner || !githubRepo) {
throw new Error('githubOwner and githubRepo are required');
}
try {
const clients = getCurrentClients();
const command = new ListAppsCommand({ maxResults: 100 });
const response = await clients.amplify.send(command);
const targetRepository = `https://github.com/${githubOwner}/${githubRepo}`;
const existingApp = response.apps?.find((app) => app.repository?.toLowerCase().includes(`${githubOwner}/${githubRepo}`.toLowerCase()));
if (existingApp) {
// Get additional app details
const getAppCommand = new GetAppCommand({ appId: existingApp.appId });
const appDetails = await clients.amplify.send(getAppCommand);
// Get branches
const listBranchesCommand = new ListBranchesCommand({
appId: existingApp.appId,
maxResults: 50
});
const branchesResponse = await clients.amplify.send(listBranchesCommand);
return {
exists: true,
app: {
appId: existingApp.appId,
name: existingApp.name,
repository: existingApp.repository,
platform: existingApp.platform,
defaultDomain: existingApp.defaultDomain,
createTime: existingApp.createTime,
updateTime: existingApp.updateTime,
branches: branchesResponse.branches || [],
totalBranches: branchesResponse.branches?.length || 0,
hasDeployments: branchesResponse.branches?.some((b) => b.totalNumberOfJobs && b.totalNumberOfJobs > 0) || false
}
};
}
return {
exists: false,
repository: targetRepository,
message: `No Amplify app found for repository ${githubOwner}/${githubRepo}`
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to check if app exists: ${errorMessage}`);
}
}
export async function handleAmplifyUpdateBuildSpec(args) {
const { appId, buildSpec = DEFAULT_BUILD_SPEC } = args;
if (!appId) {
throw new Error('appId is required');
}
try {
const clients = getCurrentClients();
// First get the current app to make sure it exists
const getAppCommand = new GetAppCommand({ appId });
const appResponse = await clients.amplify.send(getAppCommand);
if (!appResponse.app) {
throw new Error(`App ${appId} not found`);
}
// Update the build spec
const updateAppCommand = new UpdateAppCommand({
appId,
buildSpec
});
const updateResponse = await clients.amplify.send(updateAppCommand);
return {
success: true,
appId,
appName: updateResponse.app?.name,
message: 'Build spec updated successfully with Node 20+ configuration',
buildSpec: {
updated: true,
usesNode20: true,
supportsCDK: true,
hasBackendPhase: buildSpec.includes('backend:'),
hasFrontendPhase: buildSpec.includes('frontend:')
}
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to update build spec: ${errorMessage}`);
}
}
export async function handleAmplifySetupIamRole(args) {
const { appId, roleArn } = args;
if (!appId) {
throw new Error('appId is required');
}
try {
const clients = getCurrentClients();
// First get the current app
const getAppCommand = new GetAppCommand({ appId });
const appResponse = await clients.amplify.send(getAppCommand);
if (!appResponse.app) {
throw new Error(`App ${appId} not found`);
}
let targetRoleArn = roleArn;
// If no role ARN provided, try to find AmplifySSRLoggingRole
if (!targetRoleArn) {
try {
const listRolesCommand = new ListRolesCommand({
PathPrefix: '/service-role/',
MaxItems: 100
});
const rolesResponse = await clients.iam.send(listRolesCommand);
const amplifyRole = rolesResponse.Roles?.find((role) => role.RoleName?.includes('AmplifySSRLoggingRole'));
if (amplifyRole) {
targetRoleArn = amplifyRole.Arn;
}
else {
return {
success: false,
appId,
error: 'No AmplifySSRLoggingRole found. Please create one or provide a specific roleArn.',
suggestion: 'Create an AmplifySSRLoggingRole in the IAM console or provide a custom IAM role ARN.'
};
}
}
catch (iamError) {
return {
success: false,
appId,
error: 'Failed to auto-detect IAM role. Please provide a specific roleArn.',
iamError: iamError instanceof Error ? iamError.message : String(iamError)
};
}
}
// Update the app with the IAM role
const updateAppCommand = new UpdateAppCommand({
appId,
iamServiceRoleArn: targetRoleArn
});
const updateResponse = await clients.amplify.send(updateAppCommand);
return {
success: true,
appId,
appName: updateResponse.app?.name,
iamServiceRoleArn: targetRoleArn,
message: 'IAM service role assigned successfully',
roleInfo: {
assigned: true,
autoDetected: !roleArn,
roleArn: targetRoleArn
}
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to setup IAM role: ${errorMessage}`);
}
}
export async function handleAmplifyTriggerDeployment(args) {
const { appId, branchName = 'main', jobType = 'RELEASE' } = args;
if (!appId) {
throw new Error('appId is required');
}
try {
const clients = getCurrentClients();
// First verify the app and branch exist
const getAppCommand = new GetAppCommand({ appId });
const appResponse = await clients.amplify.send(getAppCommand);
if (!appResponse.app) {
throw new Error(`App ${appId} not found`);
}
// Check if branch exists
const listBranchesCommand = new ListBranchesCommand({ appId });
const branchesResponse = await clients.amplify.send(listBranchesCommand);
const branch = branchesResponse.branches?.find((b) => b.branchName === branchName);
if (!branch) {
throw new Error(`Branch '${branchName}' not found in app ${appId}`);
}
// Start the deployment job
const startJobCommand = new StartJobCommand({
appId,
branchName,
jobType: jobType
});
const jobResponse = await clients.amplify.send(startJobCommand);
return {
success: true,
appId,
appName: appResponse.app.name,
branchName,
deployment: {
jobId: jobResponse.jobSummary?.jobId,
jobType: jobResponse.jobSummary?.jobType,
status: jobResponse.jobSummary?.status,
startTime: jobResponse.jobSummary?.startTime,
commitId: jobResponse.jobSummary?.commitId,
commitMessage: jobResponse.jobSummary?.commitMessage
},
message: `Deployment triggered successfully for branch '${branchName}'`,
nextSteps: [
`Monitor deployment at: https://console.aws.amazon.com/amplify/home#/${appId}/${branchName}`,
`Use amplify_get_build_logs to check deployment progress`,
`View app at: https://${branchName}.${appId}.amplifyapp.com (when deployment completes)`
]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to trigger deployment: ${errorMessage}`);
}
}
//# sourceMappingURL=app-handlers.js.map