avocavo 1.1.2 ā 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/avocavo.js +111 -108
- package/package.json +1 -1
package/bin/avocavo.js
CHANGED
|
@@ -18,37 +18,37 @@ function secureReadFile(filePath) {
|
|
|
18
18
|
try {
|
|
19
19
|
// Resolve the absolute path
|
|
20
20
|
const resolvedPath = path.resolve(filePath);
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
// Get the current working directory
|
|
23
23
|
const cwd = process.cwd();
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
// Check if the resolved path is within the current working directory or its subdirectories
|
|
26
26
|
if (!resolvedPath.startsWith(cwd)) {
|
|
27
27
|
throw new Error('File access denied: Path traversal detected. Only files within the current directory are allowed.');
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
// Additional security: block access to sensitive system files
|
|
31
31
|
const basename = path.basename(resolvedPath).toLowerCase();
|
|
32
32
|
const forbidden = ['passwd', 'shadow', 'hosts', '.env', '.git', 'config'];
|
|
33
33
|
if (forbidden.some(name => basename.includes(name))) {
|
|
34
34
|
throw new Error('File access denied: Access to system/configuration files is not allowed.');
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// Check if file exists and is readable
|
|
38
38
|
if (!fs.existsSync(resolvedPath)) {
|
|
39
39
|
throw new Error(`File not found: ${filePath}`);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
const stats = fs.statSync(resolvedPath);
|
|
43
43
|
if (!stats.isFile()) {
|
|
44
44
|
throw new Error(`Not a file: ${filePath}`);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
// Limit file size to prevent memory issues (max 10MB)
|
|
48
48
|
if (stats.size > 10 * 1024 * 1024) {
|
|
49
49
|
throw new Error('File too large: Maximum file size is 10MB');
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
return fs.readFileSync(resolvedPath, 'utf8');
|
|
53
53
|
} catch (error) {
|
|
54
54
|
throw new Error(`Failed to read file: ${error.message}`);
|
|
@@ -77,23 +77,23 @@ program
|
|
|
77
77
|
const success = await auth.login(options.provider, false); // Disable Supabase OAuth
|
|
78
78
|
if (success) {
|
|
79
79
|
console.log(chalk.green('ā
Successfully logged in!'));
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
// Check for API keys - always check server for all keys
|
|
82
82
|
try {
|
|
83
83
|
const KeyManager = require('../lib/keys');
|
|
84
84
|
const keyManager = new KeyManager(auth, program.opts().baseUrl);
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
// Get user's API keys from server
|
|
87
87
|
const headers = await keyManager.getAuthHeaders();
|
|
88
88
|
const response = await axios.get(`${program.opts().baseUrl}/api/keys`, { headers });
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
if (response.data.keys && response.data.keys.length > 0) {
|
|
91
91
|
const keyCount = response.data.keys.length;
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
// Check if user already has a selected key
|
|
94
94
|
let currentKey = await auth.getApiKey();
|
|
95
95
|
let keyWorked = false;
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
if (currentKey) {
|
|
98
98
|
// Test the current selected key
|
|
99
99
|
const api = new NutritionAPI(currentKey, program.opts().baseUrl, 30000, auth);
|
|
@@ -109,15 +109,15 @@ program
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// If no working key and user has exactly 1 key, auto-select it
|
|
114
114
|
if (!keyWorked && keyCount === 1) {
|
|
115
115
|
console.log(chalk.cyan('š Auto-selecting your API key...'));
|
|
116
116
|
const singleKey = response.data.keys[0];
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Store the key as selected
|
|
119
119
|
await auth.storeApiKeySecurely(auth.getUserInfo().email, singleKey.api_key);
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// Test the auto-selected key
|
|
122
122
|
const api = new NutritionAPI(singleKey.api_key, program.opts().baseUrl, 30000, auth);
|
|
123
123
|
try {
|
|
@@ -184,7 +184,7 @@ program
|
|
|
184
184
|
.action(async () => {
|
|
185
185
|
try {
|
|
186
186
|
const isLoggedIn = auth.isLoggedIn();
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
if (!isLoggedIn) {
|
|
189
189
|
console.log(chalk.yellow('ā ļø Not logged in'));
|
|
190
190
|
console.log(chalk.cyan('š” Run: avocavo login'));
|
|
@@ -194,12 +194,12 @@ program
|
|
|
194
194
|
const userInfo = auth.getUserInfo();
|
|
195
195
|
console.log(chalk.green('ā
Logged in'));
|
|
196
196
|
console.log(chalk.cyan(`š§ Email: ${userInfo.email || 'Unknown'}`));
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
// Check for selected API key (separate from JWT auth token)
|
|
199
199
|
const selectedApiKey = await auth.getApiKey();
|
|
200
200
|
if (selectedApiKey && (selectedApiKey.startsWith('ak_') || selectedApiKey.startsWith('SK'))) {
|
|
201
201
|
console.log(chalk.gray(`š API Key: ${selectedApiKey.substring(0, 12)}...`));
|
|
202
|
-
|
|
202
|
+
|
|
203
203
|
// Debug storage method (hidden from users)
|
|
204
204
|
if (process.env.AVOCAVO_DEBUG) {
|
|
205
205
|
const insecureStorage = auth.config.get('insecureStorage');
|
|
@@ -210,7 +210,7 @@ program
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
// Use selected API key for nutrition API calls, JWT for auth management
|
|
215
215
|
if (selectedApiKey) {
|
|
216
216
|
try {
|
|
@@ -219,7 +219,7 @@ program
|
|
|
219
219
|
console.log(chalk.cyan(`šļø Tier: ${account.api_tier}`));
|
|
220
220
|
console.log(chalk.cyan(`š Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
|
|
221
221
|
console.log(chalk.cyan(`š
Reset: ${new Date(account.usage.reset_date).toLocaleDateString()}`));
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
// Show detailed credit buckets if available
|
|
224
224
|
if (account.credits && account.credits.total > 0) {
|
|
225
225
|
console.log(chalk.magenta('\nš° Credit Buckets:'));
|
|
@@ -256,7 +256,7 @@ program
|
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
console.log(chalk.cyan(`š” You have ${keysList.keys.length} API keys but none selected. Choose one to activate:`));
|
|
261
261
|
console.log(chalk.cyan(' avocavo keys switch'));
|
|
262
262
|
console.log(chalk.gray(' or'));
|
|
@@ -355,9 +355,9 @@ program
|
|
|
355
355
|
const api = await getApiClient();
|
|
356
356
|
const globalOpts = program.opts();
|
|
357
357
|
const verbose = options.verbose || globalOpts.verbose;
|
|
358
|
-
|
|
358
|
+
|
|
359
359
|
const result = await api.analyzeIngredient(ingredient, options.verify, verbose);
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
if (program.opts().json) {
|
|
362
362
|
console.log(JSON.stringify(result, null, 2));
|
|
363
363
|
return;
|
|
@@ -366,55 +366,55 @@ program
|
|
|
366
366
|
if (result.success) {
|
|
367
367
|
console.log(chalk.green(`ā
${result.ingredient}`));
|
|
368
368
|
console.log(formatNutrition(result.nutrition));
|
|
369
|
-
|
|
369
|
+
|
|
370
370
|
// USDA information - show basic info by default, more with flags
|
|
371
371
|
const usda = result.metadata?.usda_match;
|
|
372
372
|
if (usda) {
|
|
373
373
|
console.log(`\n š ${chalk.gray('USDA Reference:')} ${chalk.cyan(usda.description)}`);
|
|
374
374
|
console.log(` š¢ ${chalk.gray('FDC ID:')} ${chalk.yellow(usda.fdc_id)}`);
|
|
375
|
-
|
|
375
|
+
|
|
376
376
|
// Always show USDA link for verification
|
|
377
|
-
const verificationUrl = result.metadata?.usda_link ||
|
|
378
|
-
|
|
377
|
+
const verificationUrl = result.metadata?.usda_link ||
|
|
378
|
+
`https://fdc.nal.usda.gov/fdc-app.html#/food-details/${usda.fdc_id}`;
|
|
379
379
|
console.log(` š ${chalk.gray('USDA Link:')} ${chalk.underline(verificationUrl)}`);
|
|
380
|
-
|
|
380
|
+
|
|
381
381
|
// Additional details with flags
|
|
382
382
|
if (verbose || options.debug) {
|
|
383
383
|
console.log(` š ${chalk.gray('Data Type:')} ${chalk.blue(usda.data_type)}`);
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
|
-
|
|
386
|
+
|
|
387
387
|
// Estimated grams and quality information
|
|
388
388
|
const parsing = result.parsing;
|
|
389
389
|
if (parsing?.estimated_grams) {
|
|
390
390
|
console.log(`\n āļø ${chalk.gray('Estimated Grams:')} ${chalk.magenta(parsing.estimated_grams + 'g')}`);
|
|
391
391
|
}
|
|
392
|
-
|
|
392
|
+
|
|
393
393
|
// Match quality (more reliable than confidence score)
|
|
394
394
|
if (result.metadata?.match_quality) {
|
|
395
395
|
console.log(` šÆ ${chalk.gray('Match Quality:')} ${chalk.blue(result.metadata.match_quality)}`);
|
|
396
396
|
}
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
// Only show confidence with verbose flag since it can be misleadingly low for good matches
|
|
399
399
|
if ((verbose || options.debug) && result.metadata?.confidence !== undefined) {
|
|
400
400
|
const confidence = (result.metadata.confidence * 100).toFixed(1);
|
|
401
401
|
console.log(` š ${chalk.gray('Algorithm Confidence:')} ${chalk.yellow(confidence + '%')} ${chalk.gray('(internal scoring)')}`);
|
|
402
402
|
}
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
// Warning about null nutrients
|
|
405
405
|
const nullNutrients = Object.entries(result.nutrition || {})
|
|
406
406
|
.filter(([key, value]) => value === null)
|
|
407
407
|
.map(([key]) => key);
|
|
408
|
-
|
|
408
|
+
|
|
409
409
|
if (nullNutrients.length > 0) {
|
|
410
410
|
console.log(`\n ā ļø ${chalk.yellow('Note:')} ${chalk.gray(`${nullNutrients.length} nutrients unavailable in USDA database`)}`);
|
|
411
411
|
}
|
|
412
|
-
|
|
412
|
+
|
|
413
413
|
// Performance metrics only with --verbose or --debug
|
|
414
414
|
if (verbose || options.debug) {
|
|
415
415
|
console.log(formatPerformanceMetrics(result));
|
|
416
416
|
}
|
|
417
|
-
|
|
417
|
+
|
|
418
418
|
// Quick performance indicators with specific flags
|
|
419
419
|
if (options.timing) {
|
|
420
420
|
const processingTime = result.metadata?.processing_time_ms;
|
|
@@ -422,8 +422,8 @@ program
|
|
|
422
422
|
console.log(` ā±ļø ${chalk.gray('Processing Time:')} ${formatResponseTime(processingTime)}`);
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
|
|
426
|
+
|
|
427
427
|
} else {
|
|
428
428
|
console.log(chalk.red(`ā ${result.error}`));
|
|
429
429
|
process.exit(1);
|
|
@@ -447,7 +447,7 @@ program
|
|
|
447
447
|
.action(async (options) => {
|
|
448
448
|
try {
|
|
449
449
|
let ingredients = [];
|
|
450
|
-
|
|
450
|
+
|
|
451
451
|
if (options.file) {
|
|
452
452
|
const content = secureReadFile(options.file);
|
|
453
453
|
ingredients = content.split('\n').map(line => line.trim()).filter(line => line);
|
|
@@ -476,11 +476,11 @@ program
|
|
|
476
476
|
const api = await getApiClient();
|
|
477
477
|
const globalOpts = program.opts();
|
|
478
478
|
const verbose = options.verbose || globalOpts.verbose;
|
|
479
|
-
|
|
479
|
+
|
|
480
480
|
console.log(chalk.cyan(`š³ Analyzing recipe with ${ingredients.length} ingredients (${servings} servings)...`));
|
|
481
|
-
|
|
481
|
+
|
|
482
482
|
const result = await api.analyzeRecipe(ingredients, servings, verbose);
|
|
483
|
-
|
|
483
|
+
|
|
484
484
|
if (program.opts().json) {
|
|
485
485
|
console.log(JSON.stringify(result, null, 2));
|
|
486
486
|
return;
|
|
@@ -489,17 +489,17 @@ program
|
|
|
489
489
|
if (result.success) {
|
|
490
490
|
console.log(chalk.green('ā
Recipe analysis complete!'));
|
|
491
491
|
console.log('');
|
|
492
|
-
|
|
492
|
+
|
|
493
493
|
// Total nutrition
|
|
494
494
|
console.log(chalk.bold('š Total Nutrition:'));
|
|
495
495
|
console.log(formatNutrition(result.nutrition.total));
|
|
496
496
|
console.log('');
|
|
497
|
-
|
|
497
|
+
|
|
498
498
|
// Per-serving nutrition
|
|
499
499
|
console.log(chalk.bold(`š½ļø Per Serving (${servings} servings):`));
|
|
500
500
|
console.log(formatNutrition(result.nutrition.per_serving));
|
|
501
501
|
console.log('');
|
|
502
|
-
|
|
502
|
+
|
|
503
503
|
// Ingredient breakdown
|
|
504
504
|
if (result.nutrition.ingredients && result.nutrition.ingredients.length > 0) {
|
|
505
505
|
console.log(chalk.bold('š Ingredient Breakdown:'));
|
|
@@ -513,24 +513,24 @@ program
|
|
|
513
513
|
ing.nutrition ? `${ing.nutrition.fiber}g` : 'N/A',
|
|
514
514
|
ing.nutrition ? `${ing.nutrition.sodium}mg` : 'N/A'
|
|
515
515
|
]);
|
|
516
|
-
|
|
516
|
+
|
|
517
517
|
console.log(formatTable([
|
|
518
518
|
['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'],
|
|
519
519
|
...tableData
|
|
520
520
|
]));
|
|
521
|
-
|
|
521
|
+
|
|
522
522
|
// USDA verification details - show based on flags
|
|
523
|
-
const ingredientsWithUSDA = result.nutrition.ingredients.filter(ing =>
|
|
523
|
+
const ingredientsWithUSDA = result.nutrition.ingredients.filter(ing =>
|
|
524
524
|
ing.metadata?.usda_match
|
|
525
525
|
);
|
|
526
|
-
|
|
526
|
+
|
|
527
527
|
if (ingredientsWithUSDA.length > 0) {
|
|
528
528
|
console.log('');
|
|
529
|
-
|
|
529
|
+
|
|
530
530
|
// Clean default: just show count
|
|
531
531
|
const usdaCount = ingredientsWithUSDA.length;
|
|
532
532
|
console.log(chalk.gray(`šÆ USDA verified: ${usdaCount}/${ingredients.length} ingredients`));
|
|
533
|
-
|
|
533
|
+
|
|
534
534
|
// Detailed USDA info with --verbose or --verify
|
|
535
535
|
if (options.verbose || options.verify) {
|
|
536
536
|
console.log('');
|
|
@@ -539,12 +539,12 @@ program
|
|
|
539
539
|
const usda = ing.metadata.usda_match;
|
|
540
540
|
console.log(chalk.gray(` ${ing.ingredient} ā`));
|
|
541
541
|
console.log(chalk.cyan(` š ${usda.description}`));
|
|
542
|
-
|
|
542
|
+
|
|
543
543
|
if (options.verbose) {
|
|
544
544
|
console.log(chalk.yellow(` š¢ FDC ID: ${usda.fdc_id}`));
|
|
545
545
|
console.log(chalk.blue(` š Type: ${usda.data_type}`));
|
|
546
546
|
}
|
|
547
|
-
|
|
547
|
+
|
|
548
548
|
if (options.verify && usda.verification_url) {
|
|
549
549
|
console.log(chalk.underline(` š ${usda.verification_url}`));
|
|
550
550
|
}
|
|
@@ -552,20 +552,20 @@ program
|
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
|
-
|
|
555
|
+
|
|
556
556
|
// Recipe-level performance metrics (NEW) - only with --verbose
|
|
557
557
|
if (options.verbose || options.debug) {
|
|
558
558
|
console.log('');
|
|
559
559
|
console.log(chalk.gray('Recipe Performance:'));
|
|
560
|
-
|
|
560
|
+
|
|
561
561
|
if (result.usda_matches !== undefined) {
|
|
562
562
|
console.log(` šÆ ${chalk.gray('USDA Matches:')} ${chalk.green(result.usda_matches)}`);
|
|
563
563
|
}
|
|
564
|
-
|
|
564
|
+
|
|
565
565
|
if (result.processing_time_ms !== undefined) {
|
|
566
566
|
console.log(` ā±ļø ${chalk.gray('Total Time:')} ${formatResponseTime(result.processing_time_ms)}`);
|
|
567
567
|
}
|
|
568
|
-
|
|
568
|
+
|
|
569
569
|
}
|
|
570
570
|
} else {
|
|
571
571
|
console.log(chalk.red(`ā ${result.error}`));
|
|
@@ -587,7 +587,7 @@ program
|
|
|
587
587
|
.action(async (options) => {
|
|
588
588
|
try {
|
|
589
589
|
let ingredients = [];
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
if (options.file) {
|
|
592
592
|
const content = secureReadFile(options.file);
|
|
593
593
|
ingredients = content.split('\n').map(line => line.trim()).filter(line => line);
|
|
@@ -606,11 +606,11 @@ program
|
|
|
606
606
|
const api = await getApiClient();
|
|
607
607
|
const globalOpts = program.opts();
|
|
608
608
|
const verbose = options.verbose || globalOpts.verbose;
|
|
609
|
-
|
|
609
|
+
|
|
610
610
|
console.log(chalk.cyan(`ā” Batch analyzing ${ingredients.length} ingredients...`));
|
|
611
|
-
|
|
611
|
+
|
|
612
612
|
const result = await api.analyzeBatch(ingredients, verbose);
|
|
613
|
-
|
|
613
|
+
|
|
614
614
|
if (program.opts().json) {
|
|
615
615
|
console.log(JSON.stringify(result, null, 2));
|
|
616
616
|
return;
|
|
@@ -620,7 +620,7 @@ program
|
|
|
620
620
|
console.log(chalk.green(`ā
Batch analysis complete!`));
|
|
621
621
|
console.log(chalk.cyan(`š Processed: ${result.summary.successful}/${result.batch_size} ingredients`));
|
|
622
622
|
console.log('');
|
|
623
|
-
|
|
623
|
+
|
|
624
624
|
// Enhanced results table with more nutrition info
|
|
625
625
|
const tableData = result.results.map(item => [
|
|
626
626
|
item.success ? 'ā
' : 'ā',
|
|
@@ -632,12 +632,12 @@ program
|
|
|
632
632
|
item.success ? `${item.nutrition.fiber}g` : 'N/A',
|
|
633
633
|
item.success ? `${item.nutrition.sodium}mg` : 'N/A'
|
|
634
634
|
]);
|
|
635
|
-
|
|
635
|
+
|
|
636
636
|
console.log(formatTable([
|
|
637
637
|
['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'],
|
|
638
638
|
...tableData
|
|
639
639
|
]));
|
|
640
|
-
|
|
640
|
+
|
|
641
641
|
// Show USDA verification details for successful ingredients
|
|
642
642
|
if (result.results.some(item => item.success && item.metadata?.usda_match)) {
|
|
643
643
|
console.log('');
|
|
@@ -652,7 +652,7 @@ program
|
|
|
652
652
|
}
|
|
653
653
|
});
|
|
654
654
|
}
|
|
655
|
-
|
|
655
|
+
|
|
656
656
|
} else {
|
|
657
657
|
console.log(chalk.red(`ā ${result.error}`));
|
|
658
658
|
process.exit(1);
|
|
@@ -673,11 +673,11 @@ program
|
|
|
673
673
|
const api = await getApiClient();
|
|
674
674
|
const globalOpts = program.opts();
|
|
675
675
|
const verbose = options.verbose || globalOpts.verbose;
|
|
676
|
-
|
|
676
|
+
|
|
677
677
|
console.log(chalk.cyan(`š Searching UPC: ${upc}...`));
|
|
678
|
-
|
|
678
|
+
|
|
679
679
|
const result = await api.searchUPC(upc);
|
|
680
|
-
|
|
680
|
+
|
|
681
681
|
if (program.opts().json) {
|
|
682
682
|
console.log(JSON.stringify(result, null, 2));
|
|
683
683
|
return;
|
|
@@ -687,7 +687,7 @@ program
|
|
|
687
687
|
const product = result.product;
|
|
688
688
|
console.log(chalk.green(`ā
Product Found!`));
|
|
689
689
|
console.log('');
|
|
690
|
-
|
|
690
|
+
|
|
691
691
|
// Basic product info
|
|
692
692
|
if (product.product_name) {
|
|
693
693
|
console.log(`š¦ ${chalk.bold('Product:')} ${chalk.cyan(product.product_name)}`);
|
|
@@ -698,19 +698,19 @@ program
|
|
|
698
698
|
if (product.manufacturer) {
|
|
699
699
|
console.log(`š ${chalk.bold('Manufacturer:')} ${chalk.gray(product.manufacturer)}`);
|
|
700
700
|
}
|
|
701
|
-
|
|
701
|
+
|
|
702
702
|
// Data sources
|
|
703
703
|
if (product.sources && product.sources.length > 0) {
|
|
704
704
|
console.log(`š ${chalk.bold('Sources:')} ${chalk.magenta(product.sources.join(', '))}`);
|
|
705
705
|
}
|
|
706
|
-
|
|
706
|
+
|
|
707
707
|
// Categories
|
|
708
708
|
if (product.categories && product.categories.length > 0) {
|
|
709
709
|
const categoryDisplay = product.categories.slice(0, 3).join(', ');
|
|
710
710
|
const moreCategories = product.categories.length > 3 ? ` (+${product.categories.length - 3} more)` : '';
|
|
711
711
|
console.log(`š ${chalk.bold('Categories:')} ${chalk.blue(categoryDisplay)}${chalk.gray(moreCategories)}`);
|
|
712
712
|
}
|
|
713
|
-
|
|
713
|
+
|
|
714
714
|
// Serving info
|
|
715
715
|
if (product.serving_size) {
|
|
716
716
|
console.log(`š„ ${chalk.bold('Serving Size:')} ${chalk.cyan(product.serving_size)}`);
|
|
@@ -718,58 +718,61 @@ program
|
|
|
718
718
|
if (product.servings_per_container) {
|
|
719
719
|
console.log(`š¦ ${chalk.bold('Servings Per Container:')} ${chalk.cyan(product.servings_per_container)}`);
|
|
720
720
|
}
|
|
721
|
-
|
|
721
|
+
|
|
722
722
|
// Nutrition data sample (if available)
|
|
723
723
|
if (product.nutrition && typeof product.nutrition === 'object' && Object.keys(product.nutrition).length > 0) {
|
|
724
724
|
console.log('');
|
|
725
725
|
console.log(chalk.bold('š Nutrition Data Available:'));
|
|
726
|
-
|
|
726
|
+
|
|
727
727
|
// Show sample from merged nutrition if available
|
|
728
728
|
const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
|
|
729
729
|
if (nutritionData && typeof nutritionData === 'object') {
|
|
730
730
|
const sampleNutrients = Object.entries(nutritionData)
|
|
731
731
|
.slice(0, 5)
|
|
732
732
|
.filter(([key, value]) => value !== null && value !== undefined);
|
|
733
|
-
|
|
733
|
+
|
|
734
734
|
sampleNutrients.forEach(([nutrient, value]) => {
|
|
735
735
|
console.log(` ${chalk.gray(nutrient.replace(/_/g, ' ').toUpperCase())}:`, chalk.green(value));
|
|
736
736
|
});
|
|
737
|
-
|
|
737
|
+
|
|
738
738
|
const totalNutrients = Object.keys(nutritionData).length;
|
|
739
739
|
if (totalNutrients > 5) {
|
|
740
740
|
console.log(` ${chalk.gray(`... and ${totalNutrients - 5} more nutrients`)}`);
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
743
|
}
|
|
744
|
-
|
|
744
|
+
|
|
745
745
|
// Verbose information
|
|
746
746
|
if (verbose) {
|
|
747
747
|
console.log('');
|
|
748
748
|
console.log(chalk.bold('š Additional Details:'));
|
|
749
|
-
|
|
749
|
+
|
|
750
750
|
if (product.ingredients_text) {
|
|
751
|
-
const ingredientsPreview = product.ingredients_text.length > 100 ?
|
|
751
|
+
const ingredientsPreview = product.ingredients_text.length > 100 ?
|
|
752
752
|
product.ingredients_text.substring(0, 100) + '...' : product.ingredients_text;
|
|
753
753
|
console.log(` ${chalk.bold('Ingredients:')} ${chalk.gray(ingredientsPreview)}`);
|
|
754
754
|
}
|
|
755
|
-
|
|
755
|
+
|
|
756
756
|
if (product.packaging) {
|
|
757
757
|
console.log(` ${chalk.bold('Packaging:')} ${chalk.gray(product.packaging)}`);
|
|
758
758
|
}
|
|
759
|
-
|
|
760
|
-
if (product.countries
|
|
761
|
-
|
|
759
|
+
|
|
760
|
+
if (product.countries) {
|
|
761
|
+
const countriesStr = Array.isArray(product.countries) ? product.countries.join(', ') : product.countries;
|
|
762
|
+
if (countriesStr) {
|
|
763
|
+
console.log(` ${chalk.bold('Countries:')} ${chalk.gray(countriesStr)}`);
|
|
764
|
+
}
|
|
762
765
|
}
|
|
763
|
-
|
|
766
|
+
|
|
764
767
|
if (product.quality_score) {
|
|
765
768
|
console.log(` ${chalk.bold('Quality Score:')} ${chalk.cyan(product.quality_score)}`);
|
|
766
769
|
}
|
|
767
|
-
|
|
770
|
+
|
|
768
771
|
if (product.images && product.images.length > 0) {
|
|
769
772
|
console.log(` ${chalk.bold('Images:')} ${chalk.cyan(product.images.length)} available`);
|
|
770
773
|
}
|
|
771
774
|
}
|
|
772
|
-
|
|
775
|
+
|
|
773
776
|
// Performance info
|
|
774
777
|
if (result.processing_time_ms) {
|
|
775
778
|
console.log('');
|
|
@@ -778,7 +781,7 @@ program
|
|
|
778
781
|
if (result.from_cache) {
|
|
779
782
|
console.log(`š¾ ${chalk.gray('Source:')} ${chalk.green('Cache (fast response)')}`);
|
|
780
783
|
}
|
|
781
|
-
|
|
784
|
+
|
|
782
785
|
} else {
|
|
783
786
|
console.log(chalk.yellow(`ā Product not found for UPC: ${upc}`));
|
|
784
787
|
if (result.error) {
|
|
@@ -806,7 +809,7 @@ program
|
|
|
806
809
|
.action(async (options) => {
|
|
807
810
|
try {
|
|
808
811
|
let upcs = [];
|
|
809
|
-
|
|
812
|
+
|
|
810
813
|
if (options.file) {
|
|
811
814
|
const content = secureReadFile(options.file);
|
|
812
815
|
upcs = content.split('\n').map(line => line.trim()).filter(line => line);
|
|
@@ -825,11 +828,11 @@ program
|
|
|
825
828
|
const api = await getApiClient();
|
|
826
829
|
const globalOpts = program.opts();
|
|
827
830
|
const verbose = options.verbose || globalOpts.verbose;
|
|
828
|
-
|
|
831
|
+
|
|
829
832
|
console.log(chalk.cyan(`š Batch searching ${upcs.length} UPCs...`));
|
|
830
|
-
|
|
833
|
+
|
|
831
834
|
const result = await api.searchUPCBatch(upcs);
|
|
832
|
-
|
|
835
|
+
|
|
833
836
|
if (program.opts().json) {
|
|
834
837
|
console.log(JSON.stringify(result, null, 2));
|
|
835
838
|
return;
|
|
@@ -839,7 +842,7 @@ program
|
|
|
839
842
|
console.log(chalk.green(`ā
Batch search complete!`));
|
|
840
843
|
console.log(chalk.cyan(`š Found: ${result.summary.found}/${result.summary.total} products`));
|
|
841
844
|
console.log('');
|
|
842
|
-
|
|
845
|
+
|
|
843
846
|
// Results table
|
|
844
847
|
const tableData = result.results.map(item => [
|
|
845
848
|
item.success ? 'ā
' : 'ā',
|
|
@@ -848,12 +851,12 @@ program
|
|
|
848
851
|
item.success && item.product ? (item.product.brand || 'Unknown Brand') : 'N/A',
|
|
849
852
|
item.success && item.product ? item.product.sources.join(', ') : 'N/A'
|
|
850
853
|
]);
|
|
851
|
-
|
|
854
|
+
|
|
852
855
|
console.log(formatTable([
|
|
853
856
|
['Status', 'UPC', 'Product Name', 'Brand', 'Sources'],
|
|
854
857
|
...tableData
|
|
855
858
|
]));
|
|
856
|
-
|
|
859
|
+
|
|
857
860
|
// Detailed results for found products (verbose mode)
|
|
858
861
|
if (verbose) {
|
|
859
862
|
const foundProducts = result.results.filter(r => r.success && r.product);
|
|
@@ -863,7 +866,7 @@ program
|
|
|
863
866
|
foundProducts.forEach((item, index) => {
|
|
864
867
|
const product = item.product;
|
|
865
868
|
console.log(`\n${index + 1}. ${chalk.cyan(product.product_name || 'Unknown Product')} (${item.upc})`);
|
|
866
|
-
|
|
869
|
+
|
|
867
870
|
if (product.brand) {
|
|
868
871
|
console.log(` Brand: ${chalk.yellow(product.brand)}`);
|
|
869
872
|
}
|
|
@@ -873,7 +876,7 @@ program
|
|
|
873
876
|
if (product.serving_size) {
|
|
874
877
|
console.log(` Serving: ${chalk.gray(product.serving_size)}`);
|
|
875
878
|
}
|
|
876
|
-
|
|
879
|
+
|
|
877
880
|
// Nutrition sample
|
|
878
881
|
if (product.nutrition) {
|
|
879
882
|
const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
|
|
@@ -887,12 +890,12 @@ program
|
|
|
887
890
|
});
|
|
888
891
|
}
|
|
889
892
|
}
|
|
890
|
-
|
|
893
|
+
|
|
891
894
|
// Performance summary
|
|
892
895
|
if (result.processing_time_ms) {
|
|
893
896
|
console.log(`\nā±ļø ${chalk.gray('Total Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`);
|
|
894
897
|
}
|
|
895
|
-
|
|
898
|
+
|
|
896
899
|
} else {
|
|
897
900
|
console.log(chalk.red(`ā ${result.error || 'Batch search failed'}`));
|
|
898
901
|
process.exit(1);
|
|
@@ -911,7 +914,7 @@ program
|
|
|
911
914
|
try {
|
|
912
915
|
const api = await getApiClient(false); // Don't require auth for health check
|
|
913
916
|
const result = await api.healthCheck();
|
|
914
|
-
|
|
917
|
+
|
|
915
918
|
if (program.opts().json) {
|
|
916
919
|
console.log(JSON.stringify(result, null, 2));
|
|
917
920
|
return;
|
|
@@ -919,20 +922,20 @@ program
|
|
|
919
922
|
|
|
920
923
|
console.log(chalk.green(`ā
API Status: ${result.status}`));
|
|
921
924
|
console.log(chalk.cyan(`š§ Version: ${result.version}`));
|
|
922
|
-
|
|
925
|
+
|
|
923
926
|
if (result.services) {
|
|
924
927
|
console.log(chalk.bold('š Services:'));
|
|
925
928
|
Object.entries(result.services).forEach(([service, status]) => {
|
|
926
|
-
const icon = (status === 'available' || status === 'connected' || status.includes('total')) ? 'ā
' :
|
|
927
|
-
|
|
929
|
+
const icon = (status === 'available' || status === 'connected' || status.includes('total')) ? 'ā
' :
|
|
930
|
+
status === 'degraded' ? 'ā ļø' : 'ā';
|
|
928
931
|
console.log(` ${icon} ${service}: ${status}`);
|
|
929
932
|
});
|
|
930
933
|
}
|
|
931
|
-
|
|
934
|
+
|
|
932
935
|
if (result.performance) {
|
|
933
936
|
console.log(chalk.bold('ā” Performance:'));
|
|
934
937
|
console.log(` š Avg Response: ${result.performance.avg_response_time_ms}ms`);
|
|
935
|
-
|
|
938
|
+
|
|
936
939
|
if (result.performance.uptime) {
|
|
937
940
|
console.log(` ā±ļø Uptime: ${result.performance.uptime}`);
|
|
938
941
|
}
|
|
@@ -943,7 +946,7 @@ program
|
|
|
943
946
|
console.log(` š„ Active Users: ${result.performance.active_users}`);
|
|
944
947
|
}
|
|
945
948
|
}
|
|
946
|
-
|
|
949
|
+
|
|
947
950
|
if (result.dashboard_data) {
|
|
948
951
|
console.log(chalk.bold('š Dashboard Stats:'));
|
|
949
952
|
if (result.dashboard_data.total_users) {
|
|
@@ -968,12 +971,12 @@ program
|
|
|
968
971
|
// Helper function to get API client
|
|
969
972
|
async function getApiClient(requireAuth = true) {
|
|
970
973
|
const globalOpts = program.opts();
|
|
971
|
-
|
|
974
|
+
|
|
972
975
|
let apiKey = globalOpts.apiKey;
|
|
973
|
-
|
|
976
|
+
|
|
974
977
|
if (!apiKey && requireAuth) {
|
|
975
978
|
apiKey = await auth.getApiKey();
|
|
976
|
-
|
|
979
|
+
|
|
977
980
|
// Check if user is logged in (has JWT) but no API key - show helpful message
|
|
978
981
|
if (!apiKey && auth.isLoggedIn()) {
|
|
979
982
|
console.error(chalk.red('ā No API key found. You are logged in but need to create an API key.'));
|
|
@@ -984,7 +987,7 @@ async function getApiClient(requireAuth = true) {
|
|
|
984
987
|
process.exit(1);
|
|
985
988
|
}
|
|
986
989
|
}
|
|
987
|
-
|
|
990
|
+
|
|
988
991
|
return new NutritionAPI(apiKey, globalOpts.baseUrl, 30000, auth);
|
|
989
992
|
}
|
|
990
993
|
|