UNPKG

@chinchillaenterprises/mcp-amplify

Version:

AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery

162 lines 7.32 kB
import * as pty from 'node-pty'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; /** * Set an Amplify sandbox secret programmatically without value corruption * * This function uses node-pty (pseudo-terminal) to interact with the Amplify CLI * as if it were a real terminal, allowing it to handle the interactive prompt properly. * This avoids invisible character corruption that occurs with bash piping or stdin pipes. */ export async function handleAmplifySetSandboxSecret(args) { const { secretName, secretValue, sandboxIdentifier } = args; // Validation if (!secretName || typeof secretName !== 'string') { throw new Error('secretName is required and must be a string'); } if (secretValue === undefined || secretValue === null) { throw new Error('secretValue is required'); } // Convert secretValue to string (in case it's a number or boolean) const secretValueStr = String(secretValue); if (secretValueStr.length === 0) { throw new Error('secretValue cannot be empty'); } // Warn if secret is very long (SSM limit is 8KB for SecureString) const sizeInBytes = Buffer.byteLength(secretValueStr, 'utf8'); if (sizeInBytes > 8192) { throw new Error(`Secret value is too large (${sizeInBytes} bytes). AWS SSM limit is 8192 bytes for SecureString.`); } // Validate secret name format (alphanumeric, underscores, hyphens) if (!/^[A-Z0-9_-]+$/i.test(secretName)) { throw new Error('Secret name can only contain letters, numbers, underscores, and hyphens'); } try { // Check if we're in an Amplify project directory const isAmplifyProject = await checkAmplifyProject(); if (!isAmplifyProject) { throw new Error('Not in an Amplify project directory. Please run this command from your Amplify project root.'); } // Build command arguments const args_array = ['ampx', 'sandbox', 'secret', 'set', secretName]; if (sandboxIdentifier) { args_array.push('--identifier', sandboxIdentifier); } // Execute the command with controlled stdin const result = await executeAmplifySecretCommand(args_array, secretValueStr); return { success: true, secretName, sandboxIdentifier: sandboxIdentifier || '(default sandbox)', message: `Secret '${secretName}' set successfully`, details: { secretLength: secretValueStr.length, sizeInBytes, hasSpecialChars: /[^a-zA-Z0-9]/.test(secretValueStr), isMultiline: secretValueStr.includes('\n') }, verification: { command: sandboxIdentifier ? `npx ampx sandbox secret get ${secretName} --identifier ${sandboxIdentifier}` : `npx ampx sandbox secret get ${secretName}`, description: 'Run this command to verify the secret was set correctly' }, nextSteps: [ 'Secret is now available in your Lambda functions', 'Access it via process.env.' + secretName, 'Restart your sandbox if it was already running: npx ampx sandbox' ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Provide helpful error messages based on common issues if (errorMessage.includes('not found') || errorMessage.includes('command not found')) { throw new Error('Amplify CLI not found. Please install it: npm install -g @aws-amplify/cli'); } if (errorMessage.includes('credentials')) { throw new Error('AWS credentials not configured. Run: aws configure'); } if (errorMessage.includes('permission') || errorMessage.includes('Access Denied')) { throw new Error('Permission denied. Ensure your AWS IAM user has SSM permissions (ssm:PutParameter, ssm:GetParameter)'); } throw new Error(`Failed to set sandbox secret: ${errorMessage}`); } } /** * Execute the Amplify secret set command using PTY (pseudo-terminal) * * PTY creates a fake terminal that the CLI accepts as real, allowing us to * interact with interactive prompts that would otherwise hang when using stdin pipes. */ function executeAmplifySecretCommand(args, secretValue) { return new Promise((resolve, reject) => { // Determine the appropriate shell based on platform const shell = os.platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/bash'; // Create a pseudo-terminal with npx command const ptyProcess = pty.spawn('npx', args, { name: 'xterm-color', cols: 80, rows: 30, cwd: process.cwd(), env: process.env }); let output = ''; let hasWrittenSecret = false; // Listen to all data from the PTY ptyProcess.onData((data) => { output += data; // Detect the interactive prompt for secret value // The prompt looks like: "? Enter secret value: " or similar if (!hasWrittenSecret && (data.includes('Enter secret value') || data.includes('secret value:') || data.includes('Enter value'))) { // Write the secret value using \r (carriage return) for terminal ptyProcess.write(secretValue + '\r'); hasWrittenSecret = true; } }); // Handle process exit ptyProcess.onExit(({ exitCode, signal }) => { // Clean up ANSI escape codes from output for cleaner response const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, ''); // Check for success indicators if (exitCode === 0 && (cleanOutput.includes('successfully') || cleanOutput.includes('Secret set') || cleanOutput.includes('Stored secret') || hasWrittenSecret)) { resolve(cleanOutput); } else if (exitCode === 0 && !hasWrittenSecret) { // Command succeeded but we never saw the prompt - might be an issue reject(new Error('Command completed but secret prompt was not detected. Output: ' + cleanOutput)); } else if (exitCode !== 0) { reject(new Error(`Command failed with exit code ${exitCode}${signal ? ` (signal: ${signal})` : ''}: ${cleanOutput}`)); } else { resolve(cleanOutput); } }); // Timeout after 60 seconds (increased from 30 for slower systems) setTimeout(() => { ptyProcess.kill(); reject(new Error('Command timed out after 60 seconds')); }, 60000); }); } /** * Check if current directory is an Amplify project */ async function checkAmplifyProject() { try { const amplifyDir = path.join(process.cwd(), 'amplify'); const stat = await fs.stat(amplifyDir); return stat.isDirectory(); } catch (error) { return false; } } //# sourceMappingURL=sandbox-secrets-handlers.js.map