UNPKG

avocavo

Version:

Avocavo CLI - Structured nutrition data API with USDA-based calculations where applicable. Consistent nutrition estimates for apps and workflows. For informational use only. Not affiliated with or endorsed by USDA.

1,120 lines (968 loc) • 44.3 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, formatResponseTime } = 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 - Structured nutrition data with USDA-based calculations and ingredient normalization') .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 FDC source 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 FDC source 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 matched: ${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 Sources:')); 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); } }); // Free-text analyze command program .command('analyze <text>') .description('Analyze free-form meal text — AI extracts and looks up each ingredient automatically') .option('--verbose', 'Show detailed USDA info for each ingredient') .action(async (text, options) => { try { const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; console.log(chalk.cyan(`šŸ¤– Analyzing: "${text}"...`)); const result = await api.analyzeText(text, verbose); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success) { // Show what the AI extracted const extracted = result.extracted_ingredients || []; if (extracted.length > 0) { console.log(''); console.log(chalk.bold('šŸ” Detected ingredients:')); extracted.forEach(ing => console.log(chalk.gray(` • ${ing}`))); } console.log(''); console.log(chalk.green(`āœ… Analysis complete — ${result.summary.successful}/${result.batch_size || extracted.length} items matched`)); console.log(''); // Results table (same style as batch) 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' ]); if (tableData.length > 0) { console.log(formatTable([ ['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'], ...tableData ])); } // Total calories summary const totalCal = (result.results || []) .filter(i => i.success) .reduce((sum, i) => sum + (i.nutrition?.calories || 0), 0); if (totalCal > 0) { console.log(''); console.log(chalk.bold(`šŸ”„ Total Calories: ${chalk.yellow(totalCal.toFixed(1))} kcal`)); } // USDA sources (with --verbose) if (verbose) { const withUSDA = (result.results || []).filter(i => i.success && i.metadata?.usda_match); if (withUSDA.length > 0) { console.log(''); console.log(chalk.bold('šŸ”— USDA Sources:')); withUSDA.forEach(item => { const usda = item.metadata.usda_match; console.log(chalk.gray(` ${item.ingredient} → ${usda.description} (FDC: ${usda.fdc_id})`)); }); } } } else { console.log(chalk.red(`āŒ ${result.error || 'Analysis failed'}`)); console.log(chalk.cyan('šŸ’” Try rephrasing or use: avocavo batch --ingredients "item1" "item2"')); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ Analyze error: ${error.message}`)); process.exit(1); } }); // UPC/Barcode search command program .command('upc <upc>') .description('Search for product information by UPC/barcode') .option('--verbose', 'Show detailed product information') .action(async (upc, options) => { try { const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; console.log(chalk.cyan(`šŸ” Searching UPC: ${upc}...`)); const result = await api.searchUPC(upc); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success && result.product) { const product = result.product; console.log(chalk.green(`āœ… Product Found!`)); console.log(''); // Basic product info if (product.product_name) { console.log(`šŸ“¦ ${chalk.bold('Product:')} ${chalk.cyan(product.product_name)}`); } if (product.brand) { console.log(`šŸ·ļø ${chalk.bold('Brand:')} ${chalk.yellow(product.brand)}`); } if (product.manufacturer) { console.log(`šŸ­ ${chalk.bold('Manufacturer:')} ${chalk.gray(product.manufacturer)}`); } // Data sources if (product.sources && product.sources.length > 0) { console.log(`šŸ“Š ${chalk.bold('Sources:')} ${chalk.magenta(product.sources.join(', '))}`); } // Categories if (product.categories && product.categories.length > 0) { const categoryDisplay = product.categories.slice(0, 3).join(', '); const moreCategories = product.categories.length > 3 ? ` (+${product.categories.length - 3} more)` : ''; console.log(`šŸ“‹ ${chalk.bold('Categories:')} ${chalk.blue(categoryDisplay)}${chalk.gray(moreCategories)}`); } // Serving info if (product.serving_size) { console.log(`šŸ„„ ${chalk.bold('Serving Size:')} ${chalk.cyan(product.serving_size)}`); } if (product.servings_per_container) { console.log(`šŸ“¦ ${chalk.bold('Servings Per Container:')} ${chalk.cyan(product.servings_per_container)}`); } // Nutrition data sample (if available) if (product.nutrition && typeof product.nutrition === 'object' && Object.keys(product.nutrition).length > 0) { console.log(''); console.log(chalk.bold('šŸŽ Nutrition Data Available:')); // Show sample from merged nutrition if available const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition; if (nutritionData && typeof nutritionData === 'object') { const sampleNutrients = Object.entries(nutritionData) .slice(0, 5) .filter(([key, value]) => value !== null && value !== undefined); sampleNutrients.forEach(([nutrient, value]) => { console.log(` ${chalk.gray(nutrient.replace(/_/g, ' ').toUpperCase())}:`, chalk.green(value)); }); const totalNutrients = Object.keys(nutritionData).length; if (totalNutrients > 5) { console.log(` ${chalk.gray(`... and ${totalNutrients - 5} more nutrients`)}`); } } } // Verbose information if (verbose) { console.log(''); console.log(chalk.bold('šŸ“ Additional Details:')); if (product.ingredients_text) { const ingredientsPreview = product.ingredients_text.length > 100 ? product.ingredients_text.substring(0, 100) + '...' : product.ingredients_text; console.log(` ${chalk.bold('Ingredients:')} ${chalk.gray(ingredientsPreview)}`); } if (product.packaging) { console.log(` ${chalk.bold('Packaging:')} ${chalk.gray(product.packaging)}`); } if (product.countries) { const countriesStr = Array.isArray(product.countries) ? product.countries.join(', ') : product.countries; if (countriesStr) { console.log(` ${chalk.bold('Countries:')} ${chalk.gray(countriesStr)}`); } } if (product.quality_score) { console.log(` ${chalk.bold('Quality Score:')} ${chalk.cyan(product.quality_score)}`); } if (product.images && product.images.length > 0) { console.log(` ${chalk.bold('Images:')} ${chalk.cyan(product.images.length)} available`); } } // Performance info if (result.processing_time_ms) { console.log(''); console.log(`ā±ļø ${chalk.gray('Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`); } if (result.from_cache) { console.log(`šŸ’¾ ${chalk.gray('Source:')} ${chalk.green('Cache (fast response)')}`); } } else { console.log(chalk.yellow(`āŒ Product not found for UPC: ${upc}`)); if (result.error) { console.log(chalk.gray(`Error: ${result.error}`)); } console.log(''); console.log(chalk.cyan('šŸ’” Tips:')); console.log(chalk.gray(' • Check that the UPC is correct (12-13 digits)')); console.log(chalk.gray(' • Try removing any leading zeros')); console.log(chalk.gray(' • Some products may not be in our 4.4M+ product database')); } } catch (error) { console.error(chalk.red(`āŒ UPC search error: ${error.message}`)); process.exit(1); } }); // UPC batch search command program .command('upc-batch') .description('Search multiple UPCs/barcodes efficiently') .option('-u, --upcs <upcs...>', 'UPCs to search') .option('-f, --file <file>', 'Read UPCs from file (one per line)') .option('--verbose', 'Show detailed results') .action(async (options) => { try { let upcs = []; if (options.file) { const content = secureReadFile(options.file); upcs = content.split('\n').map(line => line.trim()).filter(line => line); } else if (options.upcs) { upcs = options.upcs; } else { console.log(chalk.red('āŒ Please provide UPCs via --upcs or --file')); process.exit(1); } if (upcs.length === 0) { console.log(chalk.red('āŒ No UPCs provided')); process.exit(1); } const api = await getApiClient(); const globalOpts = program.opts(); const verbose = options.verbose || globalOpts.verbose; console.log(chalk.cyan(`šŸ” Batch searching ${upcs.length} UPCs...`)); const result = await api.searchUPCBatch(upcs); if (program.opts().json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.success) { console.log(chalk.green(`āœ… Batch search complete!`)); console.log(chalk.cyan(`šŸ“Š Found: ${result.summary.found}/${result.summary.total} products`)); console.log(''); // Results table const tableData = result.results.map(item => [ item.success ? 'āœ…' : 'āŒ', item.upc, item.success && item.product ? (item.product.product_name || 'Unknown Product') : 'Not Found', item.success && item.product ? (item.product.brand || 'Unknown Brand') : 'N/A', item.success && item.product ? item.product.sources.join(', ') : 'N/A' ]); console.log(formatTable([ ['Status', 'UPC', 'Product Name', 'Brand', 'Sources'], ...tableData ])); // Detailed results for found products (verbose mode) if (verbose) { const foundProducts = result.results.filter(r => r.success && r.product); if (foundProducts.length > 0) { console.log(''); console.log(chalk.bold('šŸ“¦ Product Details:')); foundProducts.forEach((item, index) => { const product = item.product; console.log(`\n${index + 1}. ${chalk.cyan(product.product_name || 'Unknown Product')} (${item.upc})`); if (product.brand) { console.log(` Brand: ${chalk.yellow(product.brand)}`); } if (product.categories && product.categories.length > 0) { console.log(` Categories: ${chalk.blue(product.categories.slice(0, 2).join(', '))}`); } if (product.serving_size) { console.log(` Serving: ${chalk.gray(product.serving_size)}`); } // Nutrition sample if (product.nutrition) { const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition; if (nutritionData && typeof nutritionData === 'object') { const calories = nutritionData.energy_kcal || nutritionData.calories; const protein = nutritionData.protein; if (calories) console.log(` Calories: ${chalk.green(calories)}`); if (protein) console.log(` Protein: ${chalk.green(protein)}g`); } } }); } } // Performance summary if (result.processing_time_ms) { console.log(`\nā±ļø ${chalk.gray('Total Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`); } } else { console.log(chalk.red(`āŒ ${result.error || 'Batch search failed'}`)); process.exit(1); } } catch (error) { console.error(chalk.red(`āŒ UPC batch search 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); } // Helper function already imported from api.js // 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 FDC source 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 upc "041196912395" # Search UPC/barcode $ avocavo upc "041196912395" --verbose # Detailed UPC product info $ avocavo upc-batch -u "041196912395" "123456789012" # Batch UPC search $ avocavo upc-batch -f upcs.txt # UPC search from file $ 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();