import { select } from '@inquirer/prompts'; import type { Tweet } from '@podx/core/types'; export function showWelcomeBanner(): void { console.clear(); // Direct ANSI escape sequences for better compatibility const termWidth = process.stdout.columns || 80; // Clear screen and set black background console.log('\x1b[40m\x1b[37m'); // Black background, white text const podAscii = [ ' ██████╗ ██████╗ ██████╗', ' ██╔══██╗██╔═══██╗██╔══██╗', ' ██████╔╝██║ ██║██║ ██║', ' ██╔═══╝ ██║ ██║██║ ██║', ' ██║ ╚██████╔╝██████╔╝', ' ╚═╝ ╚═════╝ ╚═════╝' ]; // Top border const topBorder = '╔' + '═'.repeat(termWidth - 2) + '╗'; console.log('\x1b[31m' + topBorder + '\x1b[37m'); // Red border // Black background bar const blackBar = '\x1b[40m' + ' '.repeat(termWidth - 2) + '\x1b[37m'; console.log('\x1b[31m║\x1b[37m' + blackBar + '\x1b[31m║\x1b[37m'); // POD ASCII Art centered podAscii.forEach(line => { const padding = Math.floor((termWidth - line.length - 2) / 2); const leftPad = ' '.repeat(Math.max(0, padding)); const rightPad = ' '.repeat(Math.max(0, termWidth - line.length - 2 - padding)); console.log('\x1b[31m║\x1b[37m' + leftPad + '\x1b[31m' + line + '\x1b[37m' + rightPad + '\x1b[31m║\x1b[37m'); }); console.log('\x1b[31m║\x1b[37m' + blackBar + '\x1b[31m║\x1b[37m'); // Title lines const titleLines: Array<[string, string]> = [ ['🔥 PROMPT OR DIE 🔥', '\x1b[31m'], // Red ['🚀 POD-SCRAPE v2.0.0 🚀', '\x1b[35m'], // Magenta ['Ultimate X (Twitter) Scraper CLI', '\x1b[36m'], // Cyan ['⚡ Built for the fearless scrapers ⚡', '\x1b[33m'] // Yellow ]; titleLines.forEach(([line, color]) => { const padding = Math.floor((termWidth - (line?.length ?? 0) - 2) / 2); const leftPad = ' '.repeat(Math.max(0, padding)); const rightPad = ' '.repeat(Math.max(0, termWidth - (line?.length ?? 0) - 2 - padding)); console.log('\x1b[31m║\x1b[37m' + leftPad + color + line + '\x1b[37m' + rightPad + '\x1b[31m║\x1b[37m'); }); // Bottom border console.log('\x1b[31m║\x1b[37m' + blackBar + '\x1b[31m║\x1b[37m'); const bottomBorder = '╚' + '═'.repeat(termWidth - 2) + '╝'; console.log('\x1b[31m' + bottomBorder + '\x1b[37m'); console.log(''); console.log('\x1b[37m💀 Interactive Mode: Use ↑↓ arrows to navigate • Press Enter to select\x1b[0m'); console.log(''); } export async function showMainMenu(): Promise { const termWidth = process.stdout.columns || 80; console.log(''); console.log('🔥 COMMAND CENTER 🔥'); console.log('Navigate with ↑↓ arrows • Press Enter to select'); console.log(''); console.log('Available options:'); console.log(''); console.log('1. 🎯 SCRAPING MISSIONS - Start your scraping operations'); console.log('2. 🔍 ADVANCED SEARCH - Search Twitter with advanced filters'); console.log('3. 📊 ANALYSIS TOOLS - Analyze scraped data and crypto signals'); console.log('4. 🗃️ DATABASE OPERATIONS - Manage data storage and retrieval'); console.log('5. 🚀 SYSTEM CONTROL - Start/stop services and system management'); console.log('6. ⚙️ CONFIGURATION - Setup credentials and preferences'); console.log('7. 💀 EXIT - Retreat from the battlefield'); console.log(''); const { input } = await import('@inquirer/prompts'); try { const answer = await input({ message: 'Enter your choice (1-7):', validate: (value) => { const num = parseInt(value); return num >= 1 && num <= 7 ? true : 'Please enter a number between 1 and 7'; } }); const choice = parseInt(answer); switch (choice) { case 1: return 'scraping'; case 2: return 'search'; case 3: return 'analysis'; case 4: return 'database'; case 5: return 'system'; case 6: return 'config'; case 7: return 'exit'; default: return 'exit'; } } catch (error) { console.log('\n❌ Input error. Please try again or use command-line options.'); console.log('Available commands:'); console.log(' podx scrape - Scrape tweets from a user'); console.log(' podx search - Search Twitter'); console.log(' podx analyze - Analyze scraped data'); console.log(' podx status - Show system status'); console.log(' podx config - Setup configuration'); console.log(' podx --help - Show all commands'); return 'exit'; } } import { execSync } from 'child_process'; import { existsSync } from 'fs'; export async function checkBuildStatus(): Promise<{ typescript: boolean; distribution: boolean }> { const typescript = existsSync('dist/index.js'); const distribution = existsSync('dist'); if (!typescript) { const { Spinner } = await import('../utils/spinner'); const spinner = new Spinner('🔨 Building project...').start(); try { execSync('bun run build', { stdio: 'pipe' }); spinner.succeed('✅ Build completed'); return { typescript: true, distribution: true }; } catch (error) { spinner.fail('❌ Build failed'); return { typescript: false, distribution: false }; } } return { typescript, distribution }; } // Utility functions export const wait = (minTime: number = 1000, maxTime: number = 3000): Promise => { const waitTime = Math.floor(Math.random() * (maxTime - minTime + 1)) + minTime; return new Promise((resolve) => setTimeout(resolve, waitTime)); }; export const isValidTweet = (tweet: Tweet): boolean => { // Filter out tweets with too many hashtags, @s, or $ signs, probably spam or garbage const hashtagCount = (tweet.text?.match(/#/g) || []).length; const atCount = (tweet.text?.match(/@/g) || []).length; const dollarSignCount = (tweet.text?.match(/\$/g) || []).length; const totalCount = hashtagCount + atCount + dollarSignCount; return ( hashtagCount <= 1 && atCount <= 2 && dollarSignCount <= 1 && totalCount <= 3 ); }; import { input, password, number, confirm, checkbox } from '@inquirer/prompts'; import chalk from 'chalk'; import { config } from '@podx/core'; export async function setupCredentials(): Promise<{ username: string, password: string, email: string }> { console.log(chalk.blue('Please provide your X (Twitter) credentials:\n')); const username = await input({ message: 'Enter your X username:', default: config.x.username || '', validate: (value) => value.trim() ? true : 'Username is required' }); const userPassword = await password({ message: 'Enter your X password:', validate: (value) => value.trim() ? true : 'Password is required' }); const email = await input({ message: 'Enter your X email:', default: config.x.email || '', validate: (value) => value.trim() ? true : 'Email is required' }); return { username, password: userPassword, email }; } export async function setupScrapingConfig(): Promise<{ targetUsername: string, maxTweets: number }> { console.log(chalk.blue('\nConfigure scraping parameters:\n')); const targetUsername = await input({ message: 'Enter target X account to scrape:', default: config.scraper.targetUsername }); const maxTweets = (await number({ message: 'Maximum tweets to scrape:', default: config.scraper.maxTweets, validate: (value) => ((value ?? 0) > 0 && (value ?? 0) <= 10000) ? true : 'Must be between 1 and 10,000' })) as number; return { targetUsername, maxTweets }; } export async function confirmClean(): Promise { return await confirm({ message: 'Remove all data including scraped tweets?', default: false }); } export interface PromptDataCollectionOptions { tweets: boolean; replies: boolean; cryptoAnalysis: boolean; accountAnalysis: boolean; locationData: boolean; convexSave: boolean; } export async function setupDataCollectionOptions(targetUsername: string): Promise { console.log(chalk.blue(`\n📊 Select data to collect from @${targetUsername}:\n`)); const selectedOptions = await checkbox({ message: 'What data would you like to collect?', choices: [ { name: '🐦 Tweets - Scrape recent tweets from the account', value: 'tweets', checked: true // Default checked }, { name: '💬 Replies - Scrape replies to the account\'s tweets', value: 'replies', checked: false }, { name: '💎 Crypto Analysis - Analyze tweets for crypto signals and token mentions', value: 'cryptoAnalysis', checked: true // Default checked since this is the main feature }, { name: '👤 Account Analysis - Analyze account reputation and bot detection', value: 'accountAnalysis', checked: false }, { name: '🌍 Location Data - Extract location metadata from profiles', value: 'locationData', checked: false }, { name: '💾 Save to Convex - Automatically save data to Convex database', value: 'convexSave', checked: process.env.CONVEX_URL ? true : false // Only default if Convex is configured } ], validate: (answer) => { if (answer.length === 0) { return 'Please select at least one data collection option'; } return true; } }); return { tweets: selectedOptions.includes('tweets'), replies: selectedOptions.includes('replies'), cryptoAnalysis: selectedOptions.includes('cryptoAnalysis'), accountAnalysis: selectedOptions.includes('accountAnalysis'), locationData: selectedOptions.includes('locationData'), convexSave: selectedOptions.includes('convexSave') }; } export async function setupAdvancedOptions(): Promise<{ maxTweets: number; maxRepliesPerTweet?: number; tokenFilter?: string; includeRetweets: boolean; }> { console.log(chalk.blue('\n⚙️ Advanced Options:\n')); const maxTweets = (await number({ message: 'Maximum tweets to scrape:', default: config.scraper.maxTweets, validate: (value) => ((value ?? 0) > 0 && (value ?? 0) <= 10000) ? true : 'Must be between 1 and 10,000' })) as number; const includeRetweets = await confirm({ message: 'Include retweets in analysis?', default: false }); const wantTokenFilter = await confirm({ message: 'Filter for specific crypto token?', default: false }); let tokenFilter: string | undefined; if (wantTokenFilter) { tokenFilter = await input({ message: 'Enter token symbol (e.g., BTC, ETH, DOGE):', validate: (value) => value.trim() ? true : 'Token symbol is required' }); } let maxRepliesPerTweet: number | undefined; const needsReplies = await confirm({ message: 'Do you want to collect replies?', default: false }); if (needsReplies) { maxRepliesPerTweet = (await number({ message: 'Maximum replies per tweet:', default: 20, validate: (value) => ((value ?? 0) > 0 && (value ?? 0) <= 100) ? true : 'Must be between 1 and 100' })) as number; } const result: { maxTweets: number; maxRepliesPerTweet?: number; tokenFilter?: string; includeRetweets: boolean } = { maxTweets, includeRetweets }; if (typeof maxRepliesPerTweet === 'number') result.maxRepliesPerTweet = maxRepliesPerTweet; if (typeof tokenFilter === 'string') result.tokenFilter = tokenFilter; return result; }