import { execSync, spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import inquirer from 'inquirer'; import dotenv from 'dotenv'; import crypto from 'crypto'; import { mnemonicToAccount } from 'viem/accounts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); // Load environment variables in specific order // First load .env for main config dotenv.config({ path: '.env' }); async function validateSeedPhrase(seedPhrase) { try { // Try to create an account from the seed phrase const account = mnemonicToAccount(seedPhrase); return account.address; } catch (error) { throw new Error('Invalid seed phrase'); } } async function generateFarcasterMetadata(domain, accountAddress, seedPhrase, webhookUrl) { const trimmedDomain = domain.trim(); const header = { type: 'custody', key: accountAddress, }; const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); const payload = { domain: trimmedDomain }; const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); const account = mnemonicToAccount(seedPhrase); const signature = await account.signMessage({ message: `${encodedHeader}.${encodedPayload}` }); const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); const metadata = { accountAssociation: { header: encodedHeader, payload: encodedPayload, signature: encodedSignature }, frame: { version: "1", name: process.env.NEXT_PUBLIC_FRAME_NAME, iconUrl: `https://${trimmedDomain}/icon.png`, homeUrl: trimmedDomain, imageUrl: `https://${trimmedDomain}/opengraph-image`, buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT, splashImageUrl: `https://${trimmedDomain}/splash.png`, splashBackgroundColor: "#f7f7f7", webhookUrl, }, }; // Return stringified metadata to ensure proper JSON formatting return JSON.stringify(metadata); } async function loadEnvLocal() { try { if (fs.existsSync('.env.local')) { const { loadLocal } = await inquirer.prompt([ { type: 'confirm', name: 'loadLocal', message: 'Found .env.local - would you like to load its values in addition to .env values? (except for SEED_PHRASE, values will be written to .env)', default: true } ]); if (loadLocal) { console.log('Loading values from .env.local...'); const localEnv = dotenv.parse(fs.readFileSync('.env.local')); // Define allowed variables to load from .env.local const allowedVars = [ 'SEED_PHRASE', 'NEXT_PUBLIC_FRAME_NAME', 'NEXT_PUBLIC_FRAME_DESCRIPTION', 'NEXT_PUBLIC_FRAME_BUTTON_TEXT', 'NEYNAR_API_KEY', 'NEYNAR_CLIENT_ID' ]; // Copy allowed values except SEED_PHRASE to .env const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; let newEnvContent = envContent; for (const [key, value] of Object.entries(localEnv)) { if (allowedVars.includes(key)) { // Update process.env process.env[key] = value; // Add to .env content if not already there (except for SEED_PHRASE) if (key !== 'SEED_PHRASE' && !envContent.includes(`${key}=`)) { newEnvContent += `${key}="${value}"\n`; } } } // Write updated content to .env fs.writeFileSync('.env', newEnvContent); console.log('āœ… Values from .env.local have been written to .env'); } } } catch (error) { // Error reading .env.local, which is fine console.log('Note: No .env.local file found'); } } async function checkRequiredEnvVars() { console.log('\nšŸ“ Checking environment variables...'); console.log('Loading values from .env...'); // Load .env.local if user wants to await loadEnvLocal(); const requiredVars = [ { name: 'NEXT_PUBLIC_FRAME_NAME', message: 'Enter the name for your frame (e.g., My Cool Frame):', default: process.env.NEXT_PUBLIC_FRAME_NAME, validate: input => input.trim() !== '' || 'Frame name cannot be empty' }, { name: 'NEXT_PUBLIC_FRAME_BUTTON_TEXT', message: 'Enter the text for your frame button:', default: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT ?? 'Launch Frame', validate: input => input.trim() !== '' || 'Button text cannot be empty' } ]; const missingVars = requiredVars.filter(varConfig => !process.env[varConfig.name]); if (missingVars.length > 0) { console.log('\nāš ļø Some required information is missing. Let\'s set it up:'); for (const varConfig of missingVars) { const { value } = await inquirer.prompt([ { type: 'input', name: 'value', message: varConfig.message, default: varConfig.default, validate: varConfig.validate } ]); // Write to both process.env and .env file process.env[varConfig.name] = value; // Read existing .env content const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') : ''; // Check if the variable already exists in .env if (!envContent.includes(`${varConfig.name}=`)) { // Append the new variable to .env without extra newlines const newLine = envContent ? '\n' : ''; fs.appendFileSync('.env', `${newLine}${varConfig.name}="${value.trim()}"`); } } } // Check for seed phrase if (!process.env.SEED_PHRASE) { console.log('\nšŸ”‘ Frame Manifest Signing'); console.log('A signed manifest helps users trust your frame.'); const { seedPhrase } = await inquirer.prompt([ { type: 'password', name: 'seedPhrase', message: 'Enter your Farcaster custody account seed phrase to sign the frame manifest\n(optional -- leave blank to create an unsigned frame)\n\nSeed phrase:', default: null } ]); if (seedPhrase) { process.env.SEED_PHRASE = seedPhrase; const { storeSeedPhrase } = await inquirer.prompt([ { type: 'confirm', name: 'storeSeedPhrase', message: 'Would you like to store this seed phrase in .env.local for future use?', default: false } ]); if (storeSeedPhrase) { // Write to .env.local fs.appendFileSync('.env.local', `\nSEED_PHRASE="${seedPhrase}"`); console.log('āœ… Seed phrase stored in .env.local'); } else { console.log('ā„¹ļø Seed phrase will only be used for this deployment'); } } } } async function getGitRemote() { try { const remoteUrl = execSync('git remote get-url origin', { cwd: projectRoot, encoding: 'utf8' }).trim(); return remoteUrl; } catch (error) { return null; } } async function checkVercelCLI() { try { execSync('vercel --version', { stdio: 'ignore' }); return true; } catch (error) { return false; } } async function installVercelCLI() { console.log('Installing Vercel CLI...'); execSync('npm install -g vercel', { stdio: 'inherit' }); } async function loginToVercel() { console.log('\nšŸ”‘ Vercel Login'); console.log('You can either:'); console.log('1. Log in to an existing Vercel account'); console.log('2. Create a new Vercel account during login\n'); console.log('If creating a new account:'); console.log('1. Click "Continue with GitHub"'); console.log('2. Authorize GitHub access'); console.log('3. Complete the Vercel account setup in your browser'); console.log('4. Return here once your Vercel account is created\n'); console.log('\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account'); // Start the login process const child = spawn('vercel', ['login'], { stdio: 'inherit' }); // Wait for the login process to complete await new Promise((resolve, reject) => { child.on('close', (code) => { if (code === 0) { resolve(); } else { // Don't reject here, as the process might exit with non-zero // during the browser auth flow resolve(); } }); }); // After the browser flow completes, verify we're actually logged in // Keep checking for up to 5 minutes (increased timeout for new account setup) console.log('\nšŸ“± Waiting for login to complete...'); console.log('If you\'re creating a new account, please complete the Vercel account setup in your browser first.'); for (let i = 0; i < 150; i++) { try { execSync('vercel whoami', { stdio: 'ignore' }); console.log('āœ… Successfully logged in to Vercel!'); return true; } catch (error) { if (error.message.includes('Account not found')) { console.log('ā„¹ļø Waiting for Vercel account setup to complete...'); } // Still not logged in, wait 2 seconds before trying again await new Promise(resolve => setTimeout(resolve, 2000)); } } console.error('\nāŒ Login timed out. Please ensure you have:'); console.error('1. Completed the Vercel account setup in your browser'); console.error('2. Authorized the GitHub integration'); console.error('Then try running this script again.'); return false; } async function deployToVercel(useGitHub = false) { try { console.log('\nšŸš€ Deploying to Vercel...'); // Ensure vercel.json exists const vercelConfigPath = path.join(projectRoot, 'vercel.json'); if (!fs.existsSync(vercelConfigPath)) { console.log('šŸ“ Creating vercel.json configuration...'); fs.writeFileSync(vercelConfigPath, JSON.stringify({ buildCommand: "next build", framework: "nextjs" }, null, 2)); } // TODO: check if project already exists here // Set up Vercel project console.log('\nšŸ“¦ Setting up Vercel project...'); console.log(' An initial deployment is required to get an assigned domain that can be used in the frame manifest\n'); console.log('\nāš ļø Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n'); execSync('vercel', { cwd: projectRoot, stdio: 'inherit' }); // Load project info from .vercel/project.json const projectJson = JSON.parse(fs.readFileSync('.vercel/project.json', 'utf8')); const projectId = projectJson.projectId; // Get project details using project inspect console.log('\nšŸ” Getting project details...'); const inspectOutput = execSync(`vercel project inspect ${projectId} 2>&1`, { cwd: projectRoot, encoding: 'utf8' }); console.log('inspectOutput'); console.log(inspectOutput); // Extract project name from inspect output let projectName; let domain; const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); if (nameMatch) { projectName = nameMatch[1].trim(); domain = `${projectName}.vercel.app`; console.log('🌐 Using project name for domain:', domain); } else { // Try alternative format const altMatch = inspectOutput.match(/Found Project [^/]+\/([^\n]+)/); if (altMatch) { projectName = altMatch[1].trim(); domain = `${projectName}.vercel.app`; console.log('🌐 Using project name for domain:', domain); } else { throw new Error('Could not determine project name from inspection output'); } } // Generate frame metadata if we have a seed phrase let frameMetadata; if (process.env.SEED_PHRASE) { console.log('\nšŸ”Ø Generating frame metadata...'); const accountAddress = await validateSeedPhrase(process.env.SEED_PHRASE); // Determine webhook URL based on Neynar configuration const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `https://${domain}/api/webhook`; frameMetadata = await generateFarcasterMetadata(domain, accountAddress, process.env.SEED_PHRASE, webhookUrl); console.log('āœ… Frame metadata generated and signed'); } // Prepare environment variables const nextAuthSecret = process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex'); const vercelEnv = { // Required vars NEXTAUTH_SECRET: nextAuthSecret, AUTH_SECRET: nextAuthSecret, // Fallback for some NextAuth versions NEXTAUTH_URL: `https://${domain}`, // Add the deployment URL NEXT_PUBLIC_URL: `https://${domain}`, // Optional vars that should be set if they exist ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }), // Frame metadata and images ...(process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL && { NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL: process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL }), ...(process.env.NEXT_PUBLIC_FRAME_ICON_IMAGE_URL && { NEXT_PUBLIC_FRAME_ICON_IMAGE_URL: process.env.NEXT_PUBLIC_FRAME_ICON_IMAGE_URL }), ...(frameMetadata && { FRAME_METADATA: frameMetadata }), // Public vars ...Object.fromEntries( Object.entries(process.env) .filter(([key]) => key.startsWith('NEXT_PUBLIC_')) ) }; console.log('vercelEnv'); console.log(vercelEnv); // Add or update env vars in Vercel project console.log('\nšŸ“ Setting up environment variables...'); for (const [key, value] of Object.entries(vercelEnv)) { if (value) { try { // First try to remove the existing env var if it exists try { execSync(`vercel env rm ${key} production -y`, { cwd: projectRoot, stdio: 'ignore', env: process.env }); } catch (error) { // Ignore errors from removal (var might not exist) } // Add the new env var without newline execSync(`printf "%s" "${value}" | vercel env add ${key} production`, { cwd: projectRoot, stdio: 'inherit', env: process.env }); } catch (error) { console.warn(`āš ļø Warning: Failed to set environment variable ${key}`); } } } // Deploy the project if (useGitHub) { console.log('\nšŸ“¦ Deploying with GitHub integration...'); execSync('vercel deploy --prod --git', { cwd: projectRoot, stdio: 'inherit', env: process.env }); } else { console.log('\nšŸ“¦ Deploying local code directly...'); execSync('vercel deploy --prod', { cwd: projectRoot, stdio: 'inherit', env: process.env }); } // Verify the actual domain after deployment console.log('\nšŸ” Verifying deployment domain...'); const projectOutput = execSync('vercel project ls', { cwd: projectRoot, encoding: 'utf8', stdio: ['inherit', 'pipe', 'inherit'] // Capture stdout but show stderr }); const projectLines = projectOutput.split('\n'); console.log('Project lines:', projectLines); // Find the line containing our project name const currentProject = projectLines.find(line => line.includes(projectName) && line.includes('https://') ); console.log('Current project line:', currentProject); if (currentProject) { // Extract the domain from the line const domainMatch = currentProject.match(/https:\/\/([^\s]+)/); if (domainMatch) { const actualDomain = domainMatch[1]; if (actualDomain !== domain) { console.log(`āš ļø Actual domain (${actualDomain}) differs from assumed domain (${domain})`); console.log('šŸ”„ Updating environment variables with correct domain...'); // Update domain-dependent environment variables const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `https://${actualDomain}/api/webhook`; if (frameMetadata) { frameMetadata = await generateFarcasterMetadata(actualDomain, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); // Update FRAME_METADATA env var try { execSync(`vercel env rm FRAME_METADATA production -y`, { cwd: projectRoot, stdio: 'ignore', env: process.env }); execSync(`printf "%s" "${frameMetadata}" | vercel env add FRAME_METADATA production`, { cwd: projectRoot, stdio: 'inherit', env: process.env }); } catch (error) { console.warn('āš ļø Warning: Failed to update FRAME_METADATA with correct domain'); } } // Update NEXTAUTH_URL try { execSync(`vercel env rm NEXTAUTH_URL production -y`, { cwd: projectRoot, stdio: 'ignore', env: process.env }); execSync(`printf "%s" "https://${actualDomain}" | vercel env add NEXTAUTH_URL production`, { cwd: projectRoot, stdio: 'inherit', env: process.env }); } catch (error) { console.warn('āš ļø Warning: Failed to update NEXTAUTH_URL with correct domain'); } // Redeploy with updated environment variables console.log('\nšŸ“¦ Redeploying with correct domain...'); execSync('vercel deploy --prod', { cwd: projectRoot, stdio: 'inherit', env: process.env }); domain = actualDomain; } } else { console.warn('āš ļø Could not extract domain from project line'); } } else { console.warn('āš ļø Could not find project in Vercel project list'); } console.log('\n✨ Deployment complete! Your frame is now live at:'); console.log(`🌐 https://${domain}`); console.log('\nšŸ“ You can manage your project at https://vercel.com/dashboard'); } catch (error) { console.error('\nāŒ Deployment failed:', error.message); process.exit(1); } } async function main() { try { // Print welcome message console.log('šŸš€ Vercel Frame Deployment'); console.log('This script will deploy your frame to Vercel.'); console.log('\nThe script will:'); console.log('1. Check for required environment variables'); console.log('2. Set up a Vercel project (new or existing)'); console.log('3. Configure environment variables in Vercel'); console.log('4. Deploy and build your frame (Vercel will run the build automatically)\n'); // Check for required environment variables await checkRequiredEnvVars(); // Check for git remote const remoteUrl = await getGitRemote(); let useGitHub = false; if (remoteUrl) { console.log('\nšŸ“¦ Found GitHub repository:', remoteUrl); const { useGitHubDeploy } = await inquirer.prompt([ { type: 'confirm', name: 'useGitHubDeploy', message: 'Would you like to deploy from the GitHub repository?', default: true } ]); useGitHub = useGitHubDeploy; } else { console.log('\nāš ļø No GitHub repository found.'); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'Deploy local code directly', value: 'deploy' }, { name: 'Set up GitHub repository first', value: 'setup' } ], default: 'deploy' } ]); if (action === 'setup') { console.log('\nšŸ‘‹ Please set up your GitHub repository first:'); console.log('1. Create a new repository on GitHub'); console.log('2. Run these commands:'); console.log(' git remote add origin '); console.log(' git push -u origin main'); console.log('\nThen run this script again to deploy.'); process.exit(0); } } // Check and install Vercel CLI if needed if (!await checkVercelCLI()) { console.log('Vercel CLI not found. Installing...'); await installVercelCLI(); } // Login to Vercel console.log('pre login'); if (!await loginToVercel()) { console.error('\nāŒ Failed to log in to Vercel. Please try again.'); process.exit(1); } // Deploy to Vercel await deployToVercel(useGitHub); } catch (error) { console.error('\nāŒ Error:', error.message); process.exit(1); } } main();