import { execSync } from 'child_process'; import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import inquirer from 'inquirer'; // Load environment variables in specific order // First load .env for main config dotenv.config({ path: '.env' }); async function loadEnvLocal() { try { if (fs.existsSync('.env.local')) { const { loadLocal } = await inquirer.prompt([ { type: 'confirm', name: 'loadLocal', message: 'Found .env.local, likely created by the install script - would you like to load its values?', default: false, }, ]); const localEnv = dotenv.parse(fs.readFileSync('.env.local')); if (loadLocal) { console.log('Loading values from .env.local...'); // Copy all values to .env const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; let newEnvContent = envContent; for (const [key, value] of Object.entries(localEnv)) { // Update process.env process.env[key] = value; // Add to .env content if not already there if (!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'); } if (localEnv.SPONSOR_SIGNER) { process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; } } } catch (error) { // Error reading .env.local, which is fine console.log('Note: No .env.local file found'); } } // TODO: make sure rebuilding is supported const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); async function validateDomain(domain) { // Remove http:// or https:// if present const cleanDomain = domain.replace(/^https?:\/\//, ''); // Basic domain validation if ( !cleanDomain.match( /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/, ) ) { throw new Error('Invalid domain format'); } return cleanDomain; } async function queryNeynarApp(apiKey) { if (!apiKey) { return null; } try { const response = await fetch( 'https://api.neynar.com/portal/app_by_api_key', { headers: { 'x-api-key': apiKey, }, }, ); const data = await response.json(); return data; } catch (error) { console.error('Error querying Neynar app data:', error); return null; } } async function generateFarcasterMetadata(domain, webhookUrl) { const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); return { accountAssociation: { header: '', payload: '', signature: '', }, frame: { version: '1', name: process.env.NEXT_PUBLIC_MINI_APP_NAME, iconUrl: `https://${domain}/icon.png`, homeUrl: `https://${domain}`, imageUrl: `https://${domain}/api/opengraph-image`, buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, splashImageUrl: `https://${domain}/splash.png`, splashBackgroundColor: '#f7f7f7', webhookUrl, description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, tags, }, }; } async function main() { try { console.log('\n📝 Checking environment variables...'); console.log('Loading values from .env...'); // Load .env.local if user wants to await loadEnvLocal(); // Get domain from user const { domain } = await inquirer.prompt([ { type: 'input', name: 'domain', message: 'Enter the domain where your mini app will be deployed (e.g., example.com):', validate: async input => { try { await validateDomain(input); return true; } catch (error) { return error.message; } }, }, ]); // Get frame name from user const { frameName } = await inquirer.prompt([ { type: 'input', name: 'frameName', message: 'Enter the name for your mini app (e.g., My Cool Mini App):', default: process.env.NEXT_PUBLIC_MINI_APP_NAME, validate: input => { if (input.trim() === '') { return 'Mini app name cannot be empty'; } return true; }, }, ]); // Get button text from user const { buttonText } = await inquirer.prompt([ { type: 'input', name: 'buttonText', message: 'Enter the text for your mini app button:', default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App', validate: input => { if (input.trim() === '') { return 'Button text cannot be empty'; } return true; }, }, ]); // Get Neynar configuration let neynarApiKey = process.env.NEYNAR_API_KEY; let neynarClientId = process.env.NEYNAR_CLIENT_ID; let useNeynar = true; while (useNeynar) { if (!neynarApiKey) { const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([ { type: 'password', name: 'neynarApiKey', message: 'Enter your Neynar API key (optional - leave blank to skip):', default: null, }, ]); neynarApiKey = inputNeynarApiKey; } else { console.log('Using existing Neynar API key from .env'); } if (!neynarApiKey) { useNeynar = false; break; } // Try to get client ID from API if (!neynarClientId) { const appInfo = await queryNeynarApp(neynarApiKey); if (appInfo) { neynarClientId = appInfo.app_uuid; console.log('✅ Fetched Neynar app client ID'); break; } } // We have a client ID (either from .env or fetched from API), so we can break out of the loop if (neynarClientId) { break; } // If we get here, the API key was invalid console.log( '\n⚠️ Could not find Neynar app information. The API key may be incorrect.', ); const { retry } = await inquirer.prompt([ { type: 'confirm', name: 'retry', message: 'Would you like to try a different API key?', default: true, }, ]); // Reset for retry neynarApiKey = null; neynarClientId = null; if (!retry) { useNeynar = false; break; } } // Generate manifest console.log('\n🔨 Generating mini app manifest...'); // Determine webhook URL based on environment variables const webhookUrl = neynarApiKey && neynarClientId ? `https://api.neynar.com/f/app/${neynarClientId}/event` : `https://${domain}/api/webhook`; const metadata = await generateFarcasterMetadata(domain, webhookUrl); console.log('\n✅ Mini app manifest generated'); // Read existing .env file or create new one const envPath = path.join(projectRoot, '.env'); let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''; // Add or update environment variables const newEnvVars = [ // Base URL `NEXT_PUBLIC_URL=https://${domain}`, // Mini app metadata `NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`, `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${ process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || '' }"`, `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${ process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || '' }"`, `NEXT_PUBLIC_MINI_APP_TAGS="${ process.env.NEXT_PUBLIC_MINI_APP_TAGS || '' }"`, `NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`, // Analytics `NEXT_PUBLIC_ANALYTICS_ENABLED="${ process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false' }"`, // Neynar configuration (if it exists in current env) ...(process.env.NEYNAR_API_KEY ? [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), ...(neynarClientId ? [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), ...(process.env.SPONSOR_SIGNER ? [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] : []), // FID (if it exists in current env) ...(process.env.FID ? [`FID="${process.env.FID}"`] : []), `NEXT_PUBLIC_USE_WALLET="${ process.env.NEXT_PUBLIC_USE_WALLET || 'false' }"`, // NextAuth configuration `NEXTAUTH_SECRET="${ process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex') }"`, `NEXTAUTH_URL="https://${domain}"`, // Mini app manifest with signature `MINI_APP_METADATA=${JSON.stringify(metadata)}`, ]; // Filter out empty values and join with newlines const validEnvVars = newEnvVars.filter(line => { const [, value] = line.split('='); return value && value !== '""'; }); // Update or append each environment variable validEnvVars.forEach(varLine => { const [key] = varLine.split('='); if (envContent.includes(`${key}=`)) { envContent = envContent.replace(new RegExp(`${key}=.*`), varLine); } else { envContent += `\n${varLine}`; } }); // Write updated .env file fs.writeFileSync(envPath, envContent); console.log('\n✅ Environment variables updated'); // Run next build console.log('\nBuilding Next.js application...'); const nextBin = path.normalize( path.join(projectRoot, 'node_modules', '.bin', 'next'), ); execSync(`"${nextBin}" build`, { cwd: projectRoot, stdio: 'inherit', shell: process.platform === 'win32', }); console.log( '\n✨ Build complete! Your mini app is ready for deployment. 🪐', ); console.log( '📝 Make sure to configure the environment variables from .env in your hosting provider', ); } catch (error) { console.error('\n❌ Error:', error.message); process.exit(1); } } main();