Merge branch 'main' into shreyas-formatting

This commit is contained in:
Shreyaschorge
2025-07-14 18:55:06 +05:30
34 changed files with 2479 additions and 829 deletions

View File

@@ -1,18 +1,19 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
import crypto from 'crypto';
import fs from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import inquirer from 'inquirer';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git';
const SCRIPT_VERSION = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
).version;
// ANSI color codes
@@ -46,12 +47,12 @@ async function queryNeynarApp(apiKey) {
}
try {
const response = await fetch(
'https://api.neynar.com/portal/app_by_api_key?starter_kit=true',
`https://api.neynar.com/portal/app_by_api_key?starter_kit=true`,
{
headers: {
'x-api-key': apiKey,
},
},
}
);
const data = await response.json();
return data;
@@ -62,7 +63,7 @@ async function queryNeynarApp(apiKey) {
}
// Export the main CLI function for programmatic use
export async function init(projectName = null, autoAcceptDefaults = false) {
export async function init(projectName = null, autoAcceptDefaults = false, apiKey = null) {
printWelcomeMessage();
// Ask about Neynar usage
@@ -100,52 +101,59 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
break;
}
console.log(
'\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n',
);
let neynarKeyAnswer;
if (autoAcceptDefaults) {
neynarKeyAnswer = { neynarApiKey: null };
// Use provided API key if available, otherwise prompt for it
if (apiKey) {
neynarApiKey = apiKey;
} else {
neynarKeyAnswer = await inquirer.prompt([
{
type: 'password',
name: 'neynarApiKey',
message: 'Enter your Neynar API key (or press enter to skip):',
default: null,
},
]);
}
if (!autoAcceptDefaults) {
console.log(
'\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n'
);
}
if (neynarKeyAnswer.neynarApiKey) {
neynarApiKey = neynarKeyAnswer.neynarApiKey;
} else {
let useDemoKey;
let neynarKeyAnswer;
if (autoAcceptDefaults) {
useDemoKey = { useDemo: true };
neynarKeyAnswer = { neynarApiKey: null };
} else {
useDemoKey = await inquirer.prompt([
neynarKeyAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'useDemo',
message: 'Would you like to try the demo Neynar API key?',
default: true,
type: 'password',
name: 'neynarApiKey',
message: 'Enter your Neynar API key (or press enter to skip):',
default: null,
},
]);
}
if (useDemoKey.useDemo) {
console.warn(
'\n⚠ Note: the demo key is for development purposes only and is aggressively rate limited.',
);
console.log(
'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.',
);
console.log(
`\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`,
);
neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO';
if (neynarKeyAnswer.neynarApiKey) {
neynarApiKey = neynarKeyAnswer.neynarApiKey;
} else {
let useDemoKey;
if (autoAcceptDefaults) {
useDemoKey = { useDemo: true };
} else {
useDemoKey = await inquirer.prompt([
{
type: 'confirm',
name: 'useDemo',
message: 'Would you like to try the demo Neynar API key?',
default: true,
},
]);
}
if (useDemoKey.useDemo) {
console.warn(
'\n⚠ Note: the demo key is for development purposes only and is aggressively rate limited.'
);
console.log(
'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.'
);
console.log(
`\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`
);
neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO';
}
}
}
@@ -155,7 +163,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
break;
}
console.log(
'\n⚠ No valid API key provided. Would you like to try again?',
'\n⚠ No valid API key provided. Would you like to try again?'
);
const { retry } = await inquirer.prompt([
{
@@ -220,6 +228,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
useWallet: true,
useTunnel: true,
enableAnalytics: true,
seedPhrase: null,
sponsorSigner: false,
};
} else {
// If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options
@@ -229,7 +239,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
name: 'projectName',
message: 'What is the name of your mini app?',
default: projectName || defaultMiniAppName,
validate: input => {
validate: (input) => {
if (input.trim() === '') {
return 'Project name cannot be empty';
}
@@ -276,13 +286,13 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
message:
'Enter tags for your mini app (separate with spaces or commas, optional):',
default: '',
filter: input => {
filter: (input) => {
if (!input.trim()) return [];
// Split by both spaces and commas, trim whitespace, and filter out empty strings
return input
.split(/[,\s]+/)
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
},
},
{
@@ -290,7 +300,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
name: 'buttonText',
message: 'Enter the button text for your mini app:',
default: 'Launch Mini App',
validate: input => {
validate: (input) => {
if (input.trim() === '') {
return 'Button text cannot be empty';
}
@@ -335,6 +345,43 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
]);
answers.useTunnel = hostingAnswer.useTunnel;
// Ask about Neynar Sponsored Signers / SIWN
const sponsoredSignerAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'useSponsoredSigner',
message:
'Would you like to use Neynar Sponsored Signers and/or Sign In With Neynar (SIWN)?\n' +
'This enables the simplest, most secure, and most user-friendly Farcaster authentication for your app.\n\n' +
'Benefits of using Neynar Sponsored Signers/SIWN:\n' +
'- No auth buildout or signer management required for developers\n' +
'- Cost-effective for users (no gas for signers)\n' +
'- Users can revoke signers at any time\n' +
'- Plug-and-play for web and React Native\n' +
'- Recommended for most developers\n' +
'\n⚠ A seed phrase is required for this option.\n',
default: false,
},
]);
answers.useSponsoredSigner = sponsoredSignerAnswer.useSponsoredSigner;
if (answers.useSponsoredSigner) {
const { seedPhrase } = await inquirer.prompt([
{
type: 'password',
name: 'seedPhrase',
message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
validate: (input) => {
if (!input || input.trim().split(' ').length < 12) {
return 'Seed phrase must be at least 12 words';
}
return true;
},
},
]);
answers.seedPhrase = seedPhrase;
}
// Ask about analytics opt-out
const analyticsAnswer = await inquirer.prompt([
{
@@ -392,7 +439,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
// Update package.json
console.log('\nUpdating package.json...');
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.name = finalProjectName;
packageJson.version = '0.1.0';
@@ -409,10 +456,9 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
packageJson.dependencies = {
'@farcaster/auth-client': '>=0.3.0 <1.0.0',
'@farcaster/auth-kit': '>=0.6.0 <1.0.0',
'@farcaster/frame-core': '>=0.0.29 <1.0.0',
'@farcaster/frame-node': '>=0.0.18 <1.0.0',
'@farcaster/frame-sdk': '>=0.0.31 <1.0.0',
'@farcaster/frame-wagmi-connector': '>=0.0.19 <1.0.0',
'@farcaster/miniapp-node': '>=0.1.5 <1.0.0',
'@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0',
'@farcaster/miniapp-wagmi-connector': '^1.0.0',
'@farcaster/mini-app-solana': '>=0.0.17 <1.0.0',
'@neynar/react': '^1.2.5',
'@radix-ui/react-label': '^2.1.1',
@@ -433,24 +479,20 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
viem: '^2.23.6',
wagmi: '^2.14.12',
zod: '^3.24.2',
siwe: '^3.0.0',
};
packageJson.devDependencies = {
'@types/node': '^20',
'@types/react': '^19',
'@types/react-dom': '^19',
'@typescript-eslint/eslint-plugin': '^8.0.0',
'@typescript-eslint/parser': '^8.0.0',
'@vercel/sdk': '^1.9.0',
crypto: '^1.0.1',
eslint: '^8.57.0',
eslint: '^8',
'eslint-config-next': '15.0.3',
'eslint-config-prettier': '^9.1.0',
'eslint-plugin-prettier': '^5.2.1',
localtunnel: '^2.0.2',
'pino-pretty': '^13.0.0',
postcss: '^8',
prettier: '^3.3.3',
tailwindcss: '^3.4.1',
typescript: '^5',
};
@@ -460,15 +502,6 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
}
// Update scripts with formatting and linting
packageJson.scripts = {
...packageJson.scripts,
'lint:fix': 'next lint --fix',
format: 'prettier --write .',
'format:check': 'prettier --check .',
'type-check': 'tsc --noEmit',
};
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
// Handle .env file
@@ -489,18 +522,21 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
let constantsContent = fs.readFileSync(constantsPath, 'utf8');
// Helper function to escape single quotes in strings
const escapeString = str => str.replace(/'/g, "\\'");
const escapeString = (str) => str.replace(/'/g, "\\'");
// Helper function to safely replace constants with validation
const safeReplace = (content, pattern, replacement, constantName) => {
const match = content.match(pattern);
if (!match) {
console.log(
`⚠️ Warning: Could not update ${constantName} in constants.ts. Pattern not found.`,
`⚠️ Warning: Could not update ${constantName} in constants.ts. Pattern not found.`
);
console.log(`Pattern: ${pattern}`);
console.log(
`Expected to match in: ${content.split('\n').find(line => line.includes(constantName)) || 'Not found'}`,
`Expected to match in: ${
content.split('\n').find((line) => line.includes(constantName)) ||
'Not found'
}`
);
} else {
const newContent = content.replace(pattern, replacement);
@@ -529,43 +565,49 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
constantsContent,
patterns.APP_NAME,
`export const APP_NAME = '${escapeString(answers.projectName)}';`,
'APP_NAME',
'APP_NAME'
);
// Update APP_DESCRIPTION
constantsContent = safeReplace(
constantsContent,
patterns.APP_DESCRIPTION,
`export const APP_DESCRIPTION = '${escapeString(answers.description)}';`,
'APP_DESCRIPTION',
`export const APP_DESCRIPTION = '${escapeString(
answers.description
)}';`,
'APP_DESCRIPTION'
);
// Update APP_PRIMARY_CATEGORY (always update, null becomes empty string)
constantsContent = safeReplace(
constantsContent,
patterns.APP_PRIMARY_CATEGORY,
`export const APP_PRIMARY_CATEGORY = '${escapeString(answers.primaryCategory || '')}';`,
'APP_PRIMARY_CATEGORY',
`export const APP_PRIMARY_CATEGORY = '${escapeString(
answers.primaryCategory || ''
)}';`,
'APP_PRIMARY_CATEGORY'
);
// Update APP_TAGS
const tagsString =
answers.tags.length > 0
? `['${answers.tags.map(tag => escapeString(tag)).join("', '")}']`
? `['${answers.tags.map((tag) => escapeString(tag)).join("', '")}']`
: "['neynar', 'starter-kit', 'demo']";
constantsContent = safeReplace(
constantsContent,
patterns.APP_TAGS,
`export const APP_TAGS = ${tagsString};`,
'APP_TAGS',
'APP_TAGS'
);
// Update APP_BUTTON_TEXT (always update, use answers value)
constantsContent = safeReplace(
constantsContent,
patterns.APP_BUTTON_TEXT,
`export const APP_BUTTON_TEXT = '${escapeString(answers.buttonText || '')}';`,
'APP_BUTTON_TEXT',
`export const APP_BUTTON_TEXT = '${escapeString(
answers.buttonText || ''
)}';`,
'APP_BUTTON_TEXT'
);
// Update USE_WALLET
@@ -573,7 +615,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
constantsContent,
patterns.USE_WALLET,
`export const USE_WALLET = ${answers.useWallet};`,
'USE_WALLET',
'USE_WALLET'
);
// Update ANALYTICS_ENABLED
@@ -581,7 +623,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
constantsContent,
patterns.ANALYTICS_ENABLED,
`export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`,
'ANALYTICS_ENABLED',
'ANALYTICS_ENABLED'
);
fs.writeFileSync(constantsPath, constantsContent);
@@ -591,22 +633,25 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
fs.appendFileSync(
envPath,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
);
if (useNeynar && neynarApiKey && neynarClientId) {
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
} else if (useNeynar) {
console.log(
'\n⚠ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID',
'\n⚠ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID'
);
}
if (answers.seedPhrase) {
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
}
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
fs.unlinkSync(envExamplePath);
} else {
console.log(
'\n.env.example does not exist, skipping copy and remove operations',
'\n.env.example does not exist, skipping copy and remove operations'
);
}
@@ -651,7 +696,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
execSync('git add .', { cwd: projectPath });
execSync(
'git commit -m "initial commit from @neynar/create-farcaster-mini-app"',
{ cwd: projectPath },
{ cwd: projectPath }
);
// Calculate border length based on message length
@@ -665,4 +710,4 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
console.log('\nTo run the app:');
console.log(` cd ${finalProjectName}`);
console.log(' npm run dev\n');
}
}