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
JavaScript
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();