avocavo
Version:
Avocavo CLI - Nutrition analysis made simple. Get accurate USDA nutrition data with secure authentication.
786 lines (682 loc) ⢠31.6 kB
JavaScript
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();