UNPKG

avocavo

Version:

Avocavo CLI - Nutrition analysis made simple. Get accurate USDA nutrition data with secure authentication.

786 lines (682 loc) • 31.6 kB
#!/usr/bin/env node const { program } = require('commander'); const chalk = require('chalk'); const path = require('path'); const fs = require('fs'); const axios = require('axios'); const { NutritionAPI } = require('../lib/api'); const { AuthManager } = require('../lib/auth'); const { formatNutrition, formatPerformanceMetrics, formatUSDAMatch, formatTable } = require('../lib/formatters'); const { version } = require('../package.json'); // Initialize auth manager const auth = new AuthManager(); // Secure file reading function with path validation function secureReadFile(filePath) { try { // Resolve the absolute path const resolvedPath = path.resolve(filePath); // Get the current working directory const cwd = process.cwd(); // Check if the resolved path is within the current working directory or its subdirectories if (!resolvedPath.startsWith(cwd)) { throw new Error('File access denied: Path traversal detected. Only files within the current directory are allowed.'); } // Additional security: block access to sensitive system files const basename = path.basename(resolvedPath).toLowerCase(); const forbidden = ['passwd', 'shadow', 'hosts', '.env', '.git', 'config']; if (forbidden.some(name => basename.includes(name))) { throw new Error('File access denied: Access to system/configuration files is not allowed.'); } // Check if file exists and is readable if (!fs.existsSync(resolvedPath)) { throw new Error(`File not found: ${filePath}`); } const stats = fs.statSync(resolvedPath); if (!stats.isFile()) { throw new Error(`Not a file: ${filePath}`); } // Limit file size to prevent memory issues (max 10MB) if (stats.size > 10 * 1024 * 1024) { throw new Error('File too large: Maximum file size is 10MB'); } return fs.readFileSync(resolvedPath, 'utf8'); } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } } program .name('avocavo') .description('Avocavo Nutrition API CLI - Fast, accurate nutrition data with USDA verification') .version(version); // Global options program .option('-k, --api-key <key>', 'API key (overrides stored credentials)') .option('--base-url <url>', 'API base URL', 'https://app.avocavo.app') .option('--json', 'Output raw JSON') .option('--verbose', 'Verbose output'); // Login command program .command('login') .description('Login to Avocavo using OAuth (Google/GitHub)') .option('-p, --provider <provider>', 'OAuth provider (google or github)', 'google') .action(async (options) => { try { const success = await auth.login(options.provider, false); // Disable Supabase OAuth if (success) { console.log(chalk.green('āœ… Successfully logged in!')); // Check for API keys - always check server for all keys try { const KeyManager = require('../lib/keys'); const keyManager = new KeyManager(auth, program.opts().baseUrl); // Get user's API keys from server const headers = await keyManager.getAuthHeaders(); const response = await axios.get(`${program.opts().baseUrl}/api/keys`, { headers }); if (response.data.keys && response.data.keys.length > 0) { const keyCount = response.data.keys.length; // Check if user already has a selected key let currentKey = await auth.getApiKey(); let keyWorked = false; if (currentKey) { // Test the current selected key const api = new NutritionAPI(currentKey, program.opts().baseUrl, 30000, auth); try { const account = await api.getAccountUsage(); console.log(chalk.cyan(`šŸ“Š Account: ${account.email} (${account.api_tier} tier)`)); console.log(chalk.cyan(`šŸ“ˆ Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`)); keyWorked = true; } catch (err) { console.log(chalk.yellow('āš ļø Currently selected API key appears invalid')); if (program.opts().verbose) { console.log(chalk.gray(`Debug: ${err.message}`)); } } } // If no working key and user has exactly 1 key, auto-select it if (!keyWorked && keyCount === 1) { console.log(chalk.cyan('šŸ”„ Auto-selecting your API key...')); const singleKey = response.data.keys[0]; // Store the key as selected await auth.storeApiKeySecurely(auth.getUserInfo().email, singleKey.api_key); // Test the auto-selected key const api = new NutritionAPI(singleKey.api_key, program.opts().baseUrl, 30000, auth); try { const account = await api.getAccountUsage(); console.log(chalk.green('āœ… API key activated!')); console.log(chalk.cyan(`šŸ“Š Account: ${account.email} (${account.api_tier} tier)`)); console.log(chalk.cyan(`šŸ“ˆ Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`)); } catch (err) { console.log(chalk.yellow('āš ļø Auto-selected API key appears invalid')); console.log(chalk.cyan('šŸ’” Check your keys with: avocavo keys list')); } } else if (!keyWorked && keyCount > 1) { // Multiple keys - prompt user to select one console.log(chalk.cyan(`šŸ’” You have ${keyCount} API keys. Choose one to activate:`)); console.log(chalk.cyan(' avocavo keys switch')); console.log(chalk.gray(' or')); console.log(chalk.cyan(' avocavo keys list')); } } else { // No API keys at all console.log(chalk.yellow('āš ļø You don\'t have any API keys yet')); console.log(chalk.cyan('šŸ’” Create your first API key to start using the nutrition API:')); console.log(chalk.cyan(' avocavo keys create')); } } catch (keyCheckError) { // Fallback to create message if we can't check existing keys console.log(chalk.yellow('āš ļø Unable to check for existing API keys')); console.log(chalk.cyan('šŸ’” Create an API key to start using the nutrition API:')); console.log(chalk.cyan(' avocavo keys create')); if (program.opts().verbose) { console.log(chalk.gray(`Debug: ${keyCheckError.message}`)); } } } else { console.log(chalk.red('āŒ Login failed')); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Login error: ${error.message}`)); process.exit(1); } }); // Logout command program .command('logout') .description('Logout and remove stored credentials') .action(async () => { try { await auth.logout(); // auth.logout() already prints success message } catch (error) { console.error(chalk.red(`āŒ Logout error: ${error.message}`)); process.exit(1); } }); // OLD KEYS COMMAND REMOVED - Using new JWT-based key management below // Status command program .command('status') .description('Show login status and account information') .action(async () => { try { const isLoggedIn = auth.isLoggedIn(); if (!isLoggedIn) { console.log(chalk.yellow('āš ļø Not logged in')); console.log(chalk.cyan('šŸ’” Run: avocavo login')); return; } const userInfo = auth.getUserInfo(); console.log(chalk.green('āœ… Logged in')); console.log(chalk.cyan(`šŸ“§ Email: ${userInfo.email || 'Unknown'}`)); // Check for selected API key (separate from JWT auth token) const selectedApiKey = await auth.getApiKey(); if (selectedApiKey && (selectedApiKey.startsWith('ak_') || selectedApiKey.startsWith('SK'))) { console.log(chalk.gray(`šŸ”‘ API Key: ${selectedApiKey.substring(0, 12)}...`)); // Debug storage method (hidden from users) if (process.env.AVOCAVO_DEBUG) { const insecureStorage = auth.config.get('insecureStorage'); if (insecureStorage) { console.log(chalk.yellow('[DEBUG] šŸ“ API key using config file storage')); } else { console.log(chalk.green('[DEBUG] šŸ” API key using OS keystore')); } } } // Use selected API key for nutrition API calls, JWT for auth management if (selectedApiKey) { try { const api = new NutritionAPI(selectedApiKey, program.opts().baseUrl, 30000, auth); const account = await api.getAccountUsage(); console.log(chalk.cyan(`šŸŽŸļø Tier: ${account.api_tier}`)); console.log(chalk.cyan(`šŸ“ˆ Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`)); console.log(chalk.cyan(`šŸ“… Reset: ${new Date(account.usage.reset_date).toLocaleDateString()}`)); // Show detailed credit buckets if available if (account.credits && account.credits.total > 0) { console.log(chalk.magenta('\nšŸ’° Credit Buckets:')); if (account.credits.trial > 0) { console.log(chalk.yellow(` šŸŽ Trial: ${account.credits.trial} credits`)); } if (account.credits.monthly > 0) { console.log(chalk.blue(` šŸ“… Monthly: ${account.credits.monthly} credits`)); } if (account.credits.paid > 0) { console.log(chalk.green(` šŸ’³ Purchased: ${account.credits.paid} credits`)); } console.log(chalk.cyan(` šŸ† Total Available: ${account.credits.total} credits`)); } } catch (err) { console.log(chalk.yellow('āš ļø Could not fetch usage details')); if (program.opts().verbose) { console.log(chalk.gray(err.message)); } } } else { console.log(chalk.yellow('āš ļø Could not fetch usage details')); if (!selectedApiKey) { // Check if user has existing keys but just hasn't selected one try { const keysList = await auth.listApiKeys(); if (keysList.keys && keysList.keys.length > 0) { if (keysList.keys.length === 1) { // Auto-select if there's only one key console.log(chalk.cyan('šŸ”„ You have 1 API key. Auto-selecting it...')); const autoSelected = await auth.autoSelectSingleKey(); if (autoSelected) { console.log(chalk.green('āœ… API key activated! Run "avocavo status" again to see your usage.')); return; } } console.log(chalk.cyan(`šŸ’” You have ${keysList.keys.length} API keys but none selected. Choose one to activate:`)); console.log(chalk.cyan(' avocavo keys switch')); console.log(chalk.gray(' or')); console.log(chalk.cyan(' avocavo keys list')); } else { console.log(chalk.cyan('šŸ’” Create an API key to access usage details:')); console.log(chalk.cyan(' avocavo keys create')); } } catch (keyError) { // Fallback to create message if we can't check existing keys console.log(chalk.cyan('šŸ’” Create an API key to access usage details:')); console.log(chalk.cyan(' avocavo keys create')); } } } } catch (error) { console.error(chalk.red(`āŒ Status error: ${error.message}`)); process.exit(1); } }); // API Key management commands const keysCmd = program .command('keys') .description('Manage API keys'); keysCmd .command('list') .description('List all your API keys') .action(async () => { try { const KeyManager = require('../lib/keys'); const keyManager = new KeyManager(auth, program.opts().baseUrl); await keyManager.list(); } catch (error) { console.error(chalk.red(`āŒ Error: ${error.message}`)); process.exit(1); } }); keysCmd .command('create') .description('Create a new API key with custom name and settings') .option('-n, --name <name>', 'Custom key name (default: interactive prompt)') // Tier is account-level, not key-level .option('-d, --description <desc>', 'Key description (default: interactive prompt)') .action(async (options) => { try { const KeyManager = require('../lib/keys'); const keyManager = new KeyManager(auth, program.opts().baseUrl); await keyManager.create(options); } catch (error) { console.error(chalk.red(`āŒ Error: ${error.message}`)); process.exit(1); } }); keysCmd .command('switch [key]') .description('Switch to a different API key') .action(async (key) => { try { const KeyManager = require('../lib/keys'); const keyManager = new KeyManager(auth, program.opts().baseUrl); await keyManager.switch(key); } catch (error) { console.error(chalk.red(`āŒ Error: ${error.message}`)); process.exit(1); } }); keysCmd .command('delete [id]') .description('Delete an API key') .action(async (id) => { try { const KeyManager = require('../lib/keys'); const keyManager = new KeyManager(auth, program.opts().baseUrl); await keyManager.delete(id); } catch (error) { console.error(chalk.red(`āŒ Error: ${error.message}`)); process.exit(1); } }); // Ingredient analysis command program .command('ingredient <ingredient>') .description('Analyze a single ingredient for nutrition data') .option('-v, --verify', 'Include USDA verification URL') .option('--verbose', 'Show detailed performance metrics and USDA info') .option('--debug', 'Show all available data (same as --json)') .option('--timing', 'Show processing time') .action(async (ingredient, options) => { try { const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; const result = await api.analyzeIngredient(ingredient, options.verify, verbose); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success) { console.log(chalk.green(`āœ… ${result.ingredient}`)); console.log(formatNutrition(result.nutrition)); // USDA information - show basic info by default, more with flags const usda = result.metadata?.usda_match; if (usda) { console.log(`\n šŸ“‹ ${chalk.gray('USDA Reference:')} ${chalk.cyan(usda.description)}`); console.log(` šŸ”¢ ${chalk.gray('FDC ID:')} ${chalk.yellow(usda.fdc_id)}`); // Always show USDA link for verification const verificationUrl = result.metadata?.usda_link || `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${usda.fdc_id}`; console.log(` šŸ”— ${chalk.gray('USDA Link:')} ${chalk.underline(verificationUrl)}`); // Additional details with flags if (verbose || options.debug) { console.log(` šŸ“Š ${chalk.gray('Data Type:')} ${chalk.blue(usda.data_type)}`); } } // Estimated grams and quality information const parsing = result.parsing; if (parsing?.estimated_grams) { console.log(`\n āš–ļø ${chalk.gray('Estimated Grams:')} ${chalk.magenta(parsing.estimated_grams + 'g')}`); } // Match quality (more reliable than confidence score) if (result.metadata?.match_quality) { console.log(` šŸŽÆ ${chalk.gray('Match Quality:')} ${chalk.blue(result.metadata.match_quality)}`); } // Only show confidence with verbose flag since it can be misleadingly low for good matches if ((verbose || options.debug) && result.metadata?.confidence !== undefined) { const confidence = (result.metadata.confidence * 100).toFixed(1); console.log(` šŸ“Š ${chalk.gray('Algorithm Confidence:')} ${chalk.yellow(confidence + '%')} ${chalk.gray('(internal scoring)')}`); } // Warning about null nutrients const nullNutrients = Object.entries(result.nutrition || {}) .filter(([key, value]) => value === null) .map(([key]) => key); if (nullNutrients.length > 0) { console.log(`\n āš ļø ${chalk.yellow('Note:')} ${chalk.gray(`${nullNutrients.length} nutrients unavailable in USDA database`)}`); } // Performance metrics only with --verbose or --debug if (verbose || options.debug) { console.log(formatPerformanceMetrics(result)); } // Quick performance indicators with specific flags if (options.timing) { const processingTime = result.metadata?.processing_time_ms; if (processingTime !== undefined) { console.log(` ā±ļø ${chalk.gray('Processing Time:')} ${formatResponseTime(processingTime)}`); } } } else { console.log(chalk.red(`āŒ ${result.error}`)); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Analysis error: ${error.message}`)); process.exit(1); } }); // Recipe analysis command program .command('recipe') .description('Analyze a complete recipe for nutrition data') .option('-s, --servings <number>', 'Number of servings', '1') .option('-i, --ingredients <ingredients...>', 'Recipe ingredients') .option('-f, --file <file>', 'Read ingredients from file (one per line)') .option('-v, --verify', 'Include USDA verification URLs for ingredients') .option('--verbose', 'Show detailed performance metrics and USDA info') .option('--debug', 'Show all available data (same as --json)') .action(async (options) => { try { let ingredients = []; if (options.file) { const content = secureReadFile(options.file); ingredients = content.split('\n').map(line => line.trim()).filter(line => line); } else if (options.ingredients) { ingredients = options.ingredients; } else { // Interactive mode const inquirer = require('inquirer'); const answers = await inquirer.prompt([ { type: 'input', name: 'ingredients', message: 'Enter ingredients (comma-separated):', validate: input => input.trim() ? true : 'Please enter at least one ingredient' } ]); ingredients = answers.ingredients.split(',').map(ing => ing.trim()); } if (ingredients.length === 0) { console.log(chalk.red('āŒ No ingredients provided')); process.exit(1); } const servings = parseInt(options.servings) || 1; const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; console.log(chalk.cyan(`šŸ³ Analyzing recipe with ${ingredients.length} ingredients (${servings} servings)...`)); const result = await api.analyzeRecipe(ingredients, servings, verbose); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success) { console.log(chalk.green('āœ… Recipe analysis complete!')); console.log(''); // Total nutrition console.log(chalk.bold('šŸ“Š Total Nutrition:')); console.log(formatNutrition(result.nutrition.total)); console.log(''); // Per-serving nutrition console.log(chalk.bold(`šŸ½ļø Per Serving (${servings} servings):`)); console.log(formatNutrition(result.nutrition.per_serving)); console.log(''); // Ingredient breakdown if (result.nutrition.ingredients && result.nutrition.ingredients.length > 0) { console.log(chalk.bold('šŸ“‹ Ingredient Breakdown:')); const tableData = result.nutrition.ingredients.map(ing => [ ing.nutrition ? 'āœ…' : 'āŒ', ing.ingredient, ing.nutrition ? `${ing.nutrition.calories}` : 'N/A', ing.nutrition ? `${ing.nutrition.protein}g` : 'N/A', ing.nutrition ? `${ing.nutrition.total_fat}g` : 'N/A', ing.nutrition ? `${ing.nutrition.carbohydrates}g` : 'N/A', ing.nutrition ? `${ing.nutrition.fiber}g` : 'N/A', ing.nutrition ? `${ing.nutrition.sodium}mg` : 'N/A' ]); console.log(formatTable([ ['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'], ...tableData ])); // USDA verification details - show based on flags const ingredientsWithUSDA = result.nutrition.ingredients.filter(ing => ing.metadata?.usda_match ); if (ingredientsWithUSDA.length > 0) { console.log(''); // Clean default: just show count const usdaCount = ingredientsWithUSDA.length; console.log(chalk.gray(`šŸŽÆ USDA verified: ${usdaCount}/${ingredients.length} ingredients`)); // Detailed USDA info with --verbose or --verify if (options.verbose || options.verify) { console.log(''); console.log(chalk.bold('šŸ”— USDA Sources:')); ingredientsWithUSDA.forEach(ing => { const usda = ing.metadata.usda_match; console.log(chalk.gray(` ${ing.ingredient} →`)); console.log(chalk.cyan(` šŸ“‹ ${usda.description}`)); if (options.verbose) { console.log(chalk.yellow(` šŸ”¢ FDC ID: ${usda.fdc_id}`)); console.log(chalk.blue(` šŸ“Š Type: ${usda.data_type}`)); } if (options.verify && usda.verification_url) { console.log(chalk.underline(` šŸ”— ${usda.verification_url}`)); } }); } } } // Recipe-level performance metrics (NEW) - only with --verbose if (options.verbose || options.debug) { console.log(''); console.log(chalk.gray('Recipe Performance:')); if (result.usda_matches !== undefined) { console.log(` šŸŽÆ ${chalk.gray('USDA Matches:')} ${chalk.green(result.usda_matches)}`); } if (result.processing_time_ms !== undefined) { console.log(` ā±ļø ${chalk.gray('Total Time:')} ${formatResponseTime(result.processing_time_ms)}`); } } } else { console.log(chalk.red(`āŒ ${result.error}`)); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Recipe analysis error: ${error.message}`)); process.exit(1); } }); // Batch analysis command program .command('batch') .description('Analyze multiple ingredients efficiently') .option('-i, --ingredients <ingredients...>', 'Ingredients to analyze') .option('-f, --file <file>', 'Read ingredients from file (one per line)') .option('--verbose', 'Show detailed performance metrics and USDA info') .action(async (options) => { try { let ingredients = []; if (options.file) { const content = secureReadFile(options.file); ingredients = content.split('\n').map(line => line.trim()).filter(line => line); } else if (options.ingredients) { ingredients = options.ingredients; } else { console.log(chalk.red('āŒ Please provide ingredients via --ingredients or --file')); process.exit(1); } if (ingredients.length === 0) { console.log(chalk.red('āŒ No ingredients provided')); process.exit(1); } const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; console.log(chalk.cyan(`⚔ Batch analyzing ${ingredients.length} ingredients...`)); const result = await api.analyzeBatch(ingredients, verbose); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success) { console.log(chalk.green(`āœ… Batch analysis complete!`)); console.log(chalk.cyan(`šŸ“Š Processed: ${result.summary.successful}/${result.batch_size} ingredients`)); console.log(''); // Enhanced results table with more nutrition info const tableData = result.results.map(item => [ item.success ? 'āœ…' : 'āŒ', item.ingredient, item.success ? `${item.nutrition.calories}` : 'N/A', item.success ? `${item.nutrition.protein}g` : 'N/A', item.success ? `${item.nutrition.total_fat}g` : 'N/A', item.success ? `${item.nutrition.carbohydrates}g` : 'N/A', item.success ? `${item.nutrition.fiber}g` : 'N/A', item.success ? `${item.nutrition.sodium}mg` : 'N/A' ]); console.log(formatTable([ ['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'], ...tableData ])); // Show USDA verification details for successful ingredients if (result.results.some(item => item.success && item.metadata?.usda_match)) { console.log(''); console.log(chalk.bold('šŸ”— USDA Verification:')); result.results.forEach(item => { if (item.success && item.metadata?.usda_match) { const usda = item.metadata.usda_match; console.log(chalk.gray(` ${item.ingredient} → ${usda.description} (ID: ${usda.fdc_id})`)); if (usda.verification_url) { console.log(chalk.cyan(` 🌐 ${usda.verification_url}`)); } } }); } } else { console.log(chalk.red(`āŒ ${result.error}`)); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Batch analysis error: ${error.message}`)); process.exit(1); } }); // Health check command program .command('health') .description('Check API health and status') .action(async () => { try { const api = await getApiClient(false); // Don't require auth for health check const result = await api.healthCheck(); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } console.log(chalk.green(`āœ… API Status: ${result.status}`)); console.log(chalk.cyan(`šŸ”§ Version: ${result.version}`)); if (result.services) { console.log(chalk.bold('šŸ”Œ Services:')); Object.entries(result.services).forEach(([service, status]) => { const icon = (status === 'available' || status === 'connected' || status.includes('total')) ? 'āœ…' : status === 'degraded' ? 'āš ļø' : 'āŒ'; console.log(` ${icon} ${service}: ${status}`); }); } if (result.performance) { console.log(chalk.bold('⚔ Performance:')); console.log(` šŸ“Š Avg Response: ${result.performance.avg_response_time_ms}ms`); if (result.performance.uptime) { console.log(` ā±ļø Uptime: ${result.performance.uptime}`); } if (result.performance.api_calls_today !== undefined) { console.log(` šŸ“ˆ API Calls Today: ${result.performance.api_calls_today}`); } if (result.performance.active_users !== undefined) { console.log(` šŸ‘„ Active Users: ${result.performance.active_users}`); } } if (result.dashboard_data) { console.log(chalk.bold('šŸ“Š Dashboard Stats:')); if (result.dashboard_data.total_users) { console.log(` šŸ‘¤ Total Users: ${result.dashboard_data.total_users}`); } if (result.dashboard_data.premium_users) { console.log(` šŸ’Ž Premium Users: ${result.dashboard_data.premium_users}`); } if (result.dashboard_data.total_recipes) { console.log(` šŸ³ Total Recipes: ${result.dashboard_data.total_recipes}`); } if (result.dashboard_data.avg_rating) { console.log(` ⭐ Avg Rating: ${result.dashboard_data.avg_rating.toFixed(1)}`); } } } catch (error) { console.error(chalk.red(`āŒ Health check error: ${error.message}`)); process.exit(1); } }); // Helper function to get API client async function getApiClient(requireAuth = true) { const globalOpts = program.opts(); let apiKey = globalOpts.apiKey; if (!apiKey && requireAuth) { apiKey = await auth.getApiKey(); // Check if user is logged in (has JWT) but no API key - show helpful message if (!apiKey && auth.isLoggedIn()) { console.error(chalk.red('āŒ No API key found. You are logged in but need to create an API key.')); console.log(chalk.cyan('šŸ’” Create an API key with: avocavo keys create')); process.exit(1); } else if (!apiKey) { console.error(chalk.red('āŒ Not logged in. Run: avocavo login')); process.exit(1); } } return new NutritionAPI(apiKey, globalOpts.baseUrl, 30000, auth); } // Handle unknown commands program.on('command:*', () => { console.error(chalk.red(`āŒ Unknown command: ${program.args.join(' ')}`)); console.log(chalk.cyan('šŸ’” Run: avocavo --help')); process.exit(1); }); // Add help examples program.addHelpText('after', ` Examples: $ avocavo login # Login with Google OAuth $ avocavo login -p github # Login with GitHub OAuth $ avocavo status # Check login status $ avocavo keys list # List all API keys $ avocavo keys create # Create API key (interactive prompts) $ avocavo keys create -n "Production" # Create API key with custom name $ avocavo keys create -n "Dev" -d "Development key" # Full custom key $ avocavo keys switch # Switch active API key (interactive) $ avocavo keys delete # Delete API key (interactive) $ avocavo ingredient "1 cup rice" # Analyze single ingredient $ avocavo ingredient "1 cup rice" -v # Include USDA verification URL $ avocavo recipe -i "2 cups flour" "1 cup milk" -s 8 # Analyze recipe $ avocavo batch -i "1 cup rice" "2 tbsp oil" "4 oz chicken" # Batch analysis $ avocavo health # Check API health Authentication: $ avocavo login # OAuth login (recommended) $ avocavo -k your_api_key ingredient ... # Use API key directly Documentation: https://nutrition.avocavo.app/docs/cli `); // Parse command line program.parse();