pkg-track 1.0.0
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/CHANGELOG.md +53 -0
- package/CODE_OF_CONDUCT.md +42 -0
- package/CONTRIBUTING.md +157 -0
- package/LICENSE +21 -0
- package/PUBLISH.md +188 -0
- package/QUICKSTART.md +136 -0
- package/README.md +393 -0
- package/bin/cli.js +174 -0
- package/package.json +53 -0
- package/pkg-tracker-1.0.0.tgz +0 -0
- package/src/banner.js +34 -0
- package/src/db.js +189 -0
- package/src/gmail.js +371 -0
- package/src/open.js +209 -0
- package/src/parser.js +223 -0
- package/src/tracker.js +103 -0
package/src/open.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const open = require('open');
|
|
4
|
+
const { getAllPackages } = require('./db');
|
|
5
|
+
const { showBanner } = require('./banner');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get tracking URL for a carrier
|
|
9
|
+
*/
|
|
10
|
+
function getTrackingUrl(carrier, trackingNumber) {
|
|
11
|
+
const urls = {
|
|
12
|
+
'UPS': `https://www.ups.com/track?tracknum=${trackingNumber}`,
|
|
13
|
+
'FedEx': `https://www.fedex.com/fedextrack/?trknbr=${trackingNumber}`,
|
|
14
|
+
'USPS': `https://tools.usps.com/go/TrackConfirmAction?tLabels=${trackingNumber}`,
|
|
15
|
+
'Amazon Logistics': `https://track.amazon.com/tracking/${trackingNumber}`
|
|
16
|
+
};
|
|
17
|
+
return urls[carrier] || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get status emoji and label
|
|
22
|
+
*/
|
|
23
|
+
function getStatusDisplay(status) {
|
|
24
|
+
const statusMap = {
|
|
25
|
+
'delivered': { emoji: 'โ
', label: 'Delivered' },
|
|
26
|
+
'in_transit': { emoji: '๐', label: 'In Transit' },
|
|
27
|
+
'out_for_delivery': { emoji: '๐ฆ', label: 'Out for Delivery' },
|
|
28
|
+
'pending': { emoji: 'โณ', label: 'Pending' },
|
|
29
|
+
'delayed': { emoji: 'โ ๏ธ', label: 'Delayed' },
|
|
30
|
+
};
|
|
31
|
+
return statusMap[status] || { emoji: 'โ', label: 'Unknown' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Interactive package opener
|
|
36
|
+
*/
|
|
37
|
+
async function openPackages() {
|
|
38
|
+
// Show banner
|
|
39
|
+
showBanner();
|
|
40
|
+
|
|
41
|
+
const SUPPORTED = ['UPS', 'FedEx', 'USPS', 'Amazon Logistics'];
|
|
42
|
+
const allPackages = getAllPackages(false); // Only non-delivered
|
|
43
|
+
|
|
44
|
+
// Filter to supported carriers only
|
|
45
|
+
const packages = allPackages.filter(pkg => SUPPORTED.includes(pkg.carrier));
|
|
46
|
+
|
|
47
|
+
if (packages.length === 0) {
|
|
48
|
+
console.log(chalk.yellow('\n๐ญ No packages to track'));
|
|
49
|
+
console.log(chalk.gray('Run `pkg sync` to scan for new packages\n'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.gray(`Showing ${packages.length} undelivered package(s) from your last sync`));
|
|
54
|
+
console.log(chalk.gray('๐ก Tip: Run `pkg sync` again to get the latest updates from your email\n'));
|
|
55
|
+
|
|
56
|
+
// Pagination settings
|
|
57
|
+
const PAGE_SIZE = 5;
|
|
58
|
+
let currentPage = 0;
|
|
59
|
+
const totalPages = Math.ceil(packages.length / PAGE_SIZE);
|
|
60
|
+
|
|
61
|
+
while (true) {
|
|
62
|
+
// Get packages for current page
|
|
63
|
+
const startIdx = currentPage * PAGE_SIZE;
|
|
64
|
+
const endIdx = Math.min(startIdx + PAGE_SIZE, packages.length);
|
|
65
|
+
const pagePackages = packages.slice(startIdx, endIdx);
|
|
66
|
+
|
|
67
|
+
// Build choices for current page
|
|
68
|
+
const choices = [];
|
|
69
|
+
|
|
70
|
+
// Always show Open All at the top
|
|
71
|
+
choices.push({
|
|
72
|
+
name: chalk.bold('๐ Open all packages in browser'),
|
|
73
|
+
value: 'ALL',
|
|
74
|
+
short: 'Open all'
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Add separator if there are packages
|
|
78
|
+
if (pagePackages.length > 0) {
|
|
79
|
+
choices.push(new inquirer.Separator());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add packages for this page
|
|
83
|
+
pagePackages.forEach(pkg => {
|
|
84
|
+
const description = pkg.description || 'Package';
|
|
85
|
+
const shortDesc = description.length > 35 ? description.substring(0, 32) + '...' : description;
|
|
86
|
+
const { emoji, label } = getStatusDisplay(pkg.status);
|
|
87
|
+
|
|
88
|
+
// Format date
|
|
89
|
+
let dateStr = '';
|
|
90
|
+
if (pkg.last_email_date) {
|
|
91
|
+
const daysSince = Math.floor((Date.now() / 1000 - pkg.last_email_date) / 86400);
|
|
92
|
+
if (daysSince === 0) {
|
|
93
|
+
dateStr = 'today';
|
|
94
|
+
} else if (daysSince === 1) {
|
|
95
|
+
dateStr = 'yesterday';
|
|
96
|
+
} else if (daysSince < 7) {
|
|
97
|
+
dateStr = `${daysSince}d ago`;
|
|
98
|
+
} else {
|
|
99
|
+
const date = new Date(pkg.last_email_date * 1000);
|
|
100
|
+
dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
101
|
+
}
|
|
102
|
+
} else if (pkg.created_at) {
|
|
103
|
+
const date = new Date(pkg.created_at * 1000);
|
|
104
|
+
dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
choices.push({
|
|
108
|
+
name: `${emoji} ${shortDesc} (${pkg.carrier} - ${label}) ยท ${dateStr}`,
|
|
109
|
+
value: pkg,
|
|
110
|
+
short: shortDesc
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Add pagination controls if needed
|
|
115
|
+
if (totalPages > 1) {
|
|
116
|
+
choices.push(new inquirer.Separator());
|
|
117
|
+
|
|
118
|
+
if (currentPage < totalPages - 1) {
|
|
119
|
+
choices.push({
|
|
120
|
+
name: chalk.cyan(`โ Next page (${currentPage + 2}/${totalPages})`),
|
|
121
|
+
value: 'NEXT_PAGE',
|
|
122
|
+
short: 'Next page'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (currentPage > 0) {
|
|
127
|
+
choices.push({
|
|
128
|
+
name: chalk.cyan(`โ Previous page (${currentPage}/${totalPages})`),
|
|
129
|
+
value: 'PREV_PAGE',
|
|
130
|
+
short: 'Previous page'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Always show Exit at the very bottom
|
|
136
|
+
choices.push(new inquirer.Separator());
|
|
137
|
+
choices.push({
|
|
138
|
+
name: chalk.gray('โ Exit'),
|
|
139
|
+
value: 'EXIT',
|
|
140
|
+
short: 'Exit'
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const { selected } = await inquirer.prompt([{
|
|
144
|
+
type: 'list',
|
|
145
|
+
name: 'selected',
|
|
146
|
+
message: `Page ${currentPage + 1}/${totalPages}:`,
|
|
147
|
+
choices,
|
|
148
|
+
pageSize: 12
|
|
149
|
+
}]);
|
|
150
|
+
|
|
151
|
+
if (selected === 'EXIT') {
|
|
152
|
+
console.log(chalk.gray('\nGoodbye! ๐\n'));
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (selected === 'NEXT_PAGE') {
|
|
157
|
+
currentPage++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (selected === 'PREV_PAGE') {
|
|
162
|
+
currentPage--;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (selected === 'ALL') {
|
|
167
|
+
console.log(chalk.blue('\n๐ Opening all packages in browser...\n'));
|
|
168
|
+
for (const pkg of packages) {
|
|
169
|
+
const url = getTrackingUrl(pkg.carrier, pkg.tracking_number);
|
|
170
|
+
if (url) {
|
|
171
|
+
console.log(chalk.gray(` Opening ${pkg.description}...`));
|
|
172
|
+
await open(url);
|
|
173
|
+
// Small delay to avoid overwhelming the browser
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
console.log(chalk.green('\nโ Opened all packages\n'));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Open individual package
|
|
182
|
+
const url = getTrackingUrl(selected.carrier, selected.tracking_number);
|
|
183
|
+
if (url) {
|
|
184
|
+
console.log(chalk.blue(`\n๐ Opening ${selected.description} in browser...\n`));
|
|
185
|
+
await open(url);
|
|
186
|
+
|
|
187
|
+
// Ask if they want to track another
|
|
188
|
+
const { another } = await inquirer.prompt([{
|
|
189
|
+
type: 'confirm',
|
|
190
|
+
name: 'another',
|
|
191
|
+
message: 'Track another package?',
|
|
192
|
+
default: false
|
|
193
|
+
}]);
|
|
194
|
+
|
|
195
|
+
if (!another) {
|
|
196
|
+
console.log(chalk.gray('\nGoodbye! ๐\n'));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reset to first page when returning to menu
|
|
201
|
+
currentPage = 0;
|
|
202
|
+
console.log(); // Add spacing
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
openPackages
|
|
209
|
+
};
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex patterns for supported shipping carriers
|
|
3
|
+
* UPS, FedEx, USPS, Amazon Logistics
|
|
4
|
+
*/
|
|
5
|
+
const TRACKING_PATTERNS = {
|
|
6
|
+
ups: {
|
|
7
|
+
pattern: /\b(1Z[A-Z0-9]{16})\b/gi,
|
|
8
|
+
carrier: 'UPS'
|
|
9
|
+
},
|
|
10
|
+
usps: {
|
|
11
|
+
pattern: /\b(9[0-9]{21}|92[0-9]{18}|94[0-9]{20}|82[0-9]{8})\b/gi,
|
|
12
|
+
carrier: 'USPS'
|
|
13
|
+
},
|
|
14
|
+
amazon: {
|
|
15
|
+
pattern: /\b(TBA\d{12})\b/gi,
|
|
16
|
+
carrier: 'Amazon Logistics'
|
|
17
|
+
},
|
|
18
|
+
fedex: {
|
|
19
|
+
// FedEx is tricky - only match if we see FedEx keywords nearby
|
|
20
|
+
pattern: /\b(\d{12}|\d{15}|\d{20})\b/g,
|
|
21
|
+
carrier: 'FedEx',
|
|
22
|
+
requiresContext: true // Needs "fedex" in surrounding text
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract tracking numbers from email text
|
|
28
|
+
* @param {string} text - Email body or subject
|
|
29
|
+
* @returns {Array} Array of { trackingNumber, carrier } objects
|
|
30
|
+
*/
|
|
31
|
+
function extractTrackingNumbers(text) {
|
|
32
|
+
const found = [];
|
|
33
|
+
const seen = new Set(); // Avoid duplicates
|
|
34
|
+
const lowerText = text.toLowerCase();
|
|
35
|
+
|
|
36
|
+
// Try each carrier pattern
|
|
37
|
+
for (const [key, config] of Object.entries(TRACKING_PATTERNS)) {
|
|
38
|
+
const { pattern, carrier, requiresContext } = config;
|
|
39
|
+
|
|
40
|
+
// For FedEx, only match if "fedex" appears in text
|
|
41
|
+
if (requiresContext && !lowerText.includes('fedex')) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const matches = text.matchAll(pattern);
|
|
46
|
+
|
|
47
|
+
for (const match of matches) {
|
|
48
|
+
const trackingNumber = match[1];
|
|
49
|
+
|
|
50
|
+
// Basic validation to reduce false positives
|
|
51
|
+
if (isValidTrackingNumber(trackingNumber, key) && !seen.has(trackingNumber)) {
|
|
52
|
+
found.push({ trackingNumber, carrier });
|
|
53
|
+
seen.add(trackingNumber);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Additional validation to reduce false positives
|
|
63
|
+
*/
|
|
64
|
+
function isValidTrackingNumber(number, carrierKey) {
|
|
65
|
+
// UPS: Always starts with 1Z
|
|
66
|
+
if (carrierKey === 'ups') {
|
|
67
|
+
return number.startsWith('1Z');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// USPS: Specific prefixes
|
|
71
|
+
if (carrierKey === 'usps') {
|
|
72
|
+
return /^(9[0-9]|92|94|82)/.test(number);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Amazon: TBA prefix + exactly 12 digits after
|
|
76
|
+
if (carrierKey === 'amazon') {
|
|
77
|
+
return number.startsWith('TBA') && number.length === 15;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// FedEx: Length-based validation (very strict to avoid false positives)
|
|
81
|
+
if (carrierKey === 'fedex') {
|
|
82
|
+
const len = number.length;
|
|
83
|
+
// Only accept if exactly 12, 15, or 20 digits
|
|
84
|
+
return (len === 12 || len === 15 || len === 20) && /^\d+$/.test(number);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect retailer from email headers and body
|
|
92
|
+
*/
|
|
93
|
+
function detectRetailer(emailData) {
|
|
94
|
+
const from = emailData.from.toLowerCase();
|
|
95
|
+
const subject = emailData.subject.toLowerCase();
|
|
96
|
+
|
|
97
|
+
const retailers = {
|
|
98
|
+
'Amazon': ['amazon.com', 'amazon'],
|
|
99
|
+
'Target': ['target.com', 'target'],
|
|
100
|
+
'Walmart': ['walmart.com', 'walmart'],
|
|
101
|
+
'Etsy': ['etsy.com', 'etsy'],
|
|
102
|
+
'eBay': ['ebay.com', 'ebay'],
|
|
103
|
+
'Best Buy': ['bestbuy.com', 'best buy'],
|
|
104
|
+
'Apple': ['apple.com', 'apple'],
|
|
105
|
+
'Nike': ['nike.com', 'nike'],
|
|
106
|
+
'Adidas': ['adidas.com', 'adidas'],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (const [retailer, keywords] of Object.entries(retailers)) {
|
|
110
|
+
if (keywords.some(kw => from.includes(kw) || subject.includes(kw))) {
|
|
111
|
+
return retailer;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return 'Unknown';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract shipped date from email
|
|
120
|
+
*/
|
|
121
|
+
function extractShippedDate(emailData) {
|
|
122
|
+
const { subject, body, date } = emailData;
|
|
123
|
+
const text = `${subject} ${body}`;
|
|
124
|
+
|
|
125
|
+
// Look for common date patterns in shipping emails
|
|
126
|
+
const datePatterns = [
|
|
127
|
+
/shipped on ([A-Z][a-z]+\s+\d{1,2},?\s+\d{4})/i,
|
|
128
|
+
/ship date:?\s*([A-Z][a-z]+\s+\d{1,2},?\s+\d{4})/i,
|
|
129
|
+
/estimated ship date:?\s*([A-Z][a-z]+\s+\d{1,2},?\s+\d{4})/i,
|
|
130
|
+
/(\d{1,2}\/\d{1,2}\/\d{4})/g, // MM/DD/YYYY
|
|
131
|
+
/(\d{4}-\d{2}-\d{2})/g, // YYYY-MM-DD
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const pattern of datePatterns) {
|
|
135
|
+
const match = text.match(pattern);
|
|
136
|
+
if (match) {
|
|
137
|
+
try {
|
|
138
|
+
const parsedDate = new Date(match[1] || match[0]);
|
|
139
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
140
|
+
return Math.floor(parsedDate.getTime() / 1000);
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Continue trying other patterns
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback to email date (when email was received)
|
|
149
|
+
if (date) {
|
|
150
|
+
try {
|
|
151
|
+
const parsedDate = new Date(date);
|
|
152
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
153
|
+
return Math.floor(parsedDate.getTime() / 1000);
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// ignore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Last resort: use current time
|
|
161
|
+
return Math.floor(Date.now() / 1000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract product/order details from email body
|
|
166
|
+
*/
|
|
167
|
+
function extractProductDetails(emailData) {
|
|
168
|
+
const { subject, body } = emailData;
|
|
169
|
+
|
|
170
|
+
// Try to extract product name from subject first
|
|
171
|
+
const subjectPatterns = [
|
|
172
|
+
/shipped:?\s*(.+?)(?:\s*-\s*tracking|$)/i,
|
|
173
|
+
/order.*?:\s*(.+?)(?:\s*has shipped|$)/i,
|
|
174
|
+
/your\s+(.+?)\s+(?:has|have) shipped/i,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const pattern of subjectPatterns) {
|
|
178
|
+
const match = subject.match(pattern);
|
|
179
|
+
if (match && match[1]) {
|
|
180
|
+
const cleaned = match[1].trim();
|
|
181
|
+
if (cleaned.length > 3 && cleaned.length < 100) {
|
|
182
|
+
return cleaned;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try to extract from body
|
|
188
|
+
const bodyPatterns = [
|
|
189
|
+
/item:?\s*(.+?)(?:\n|quantity|price|tracking)/i,
|
|
190
|
+
/product:?\s*(.+?)(?:\n|quantity|price|tracking)/i,
|
|
191
|
+
/order summary:?\s*(.+?)(?:\n\n|quantity|price|tracking)/i,
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const pattern of bodyPatterns) {
|
|
195
|
+
const match = body.match(pattern);
|
|
196
|
+
if (match && match[1]) {
|
|
197
|
+
const cleaned = match[1].trim().replace(/\s+/g, ' ');
|
|
198
|
+
if (cleaned.length > 3 && cleaned.length < 100) {
|
|
199
|
+
return cleaned;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fallback to a cleaned-up subject line
|
|
205
|
+
const cleanedSubject = subject
|
|
206
|
+
.replace(/^(re:|fwd?:)\s*/gi, '')
|
|
207
|
+
.replace(/shipped|tracking|delivered|order confirmation/gi, '')
|
|
208
|
+
.replace(/\s+/g, ' ')
|
|
209
|
+
.trim();
|
|
210
|
+
|
|
211
|
+
if (cleanedSubject.length > 3 && cleanedSubject.length < 100) {
|
|
212
|
+
return cleanedSubject;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return 'Package';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
extractTrackingNumbers,
|
|
220
|
+
detectRetailer,
|
|
221
|
+
extractShippedDate,
|
|
222
|
+
extractProductDetails
|
|
223
|
+
};
|
package/src/tracker.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { upsertPackage } = require('./db');
|
|
2
|
+
const { extractTrackingNumbers } = require('./parser');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
const CARRIERS = [
|
|
7
|
+
'UPS',
|
|
8
|
+
'FedEx',
|
|
9
|
+
'USPS',
|
|
10
|
+
'Amazon Logistics'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manually add a package with interactive prompts
|
|
15
|
+
*/
|
|
16
|
+
async function addPackage(trackingNumber, options = {}) {
|
|
17
|
+
console.log(chalk.blue('\n๐ฆ Manual Package Entry\n'));
|
|
18
|
+
console.log(chalk.gray('Add a tracking number that wasn\'t found in your email'));
|
|
19
|
+
console.log(chalk.gray('Supports: UPS, FedEx, USPS, Amazon Logistics\n'));
|
|
20
|
+
|
|
21
|
+
// Sanitize input - remove whitespace and ensure alphanumeric
|
|
22
|
+
trackingNumber = trackingNumber.trim().toUpperCase();
|
|
23
|
+
|
|
24
|
+
// Basic security check - only allow alphanumeric tracking numbers
|
|
25
|
+
if (!/^[A-Z0-9]+$/.test(trackingNumber)) {
|
|
26
|
+
throw new Error('Invalid tracking number format. Only letters and numbers allowed.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Length check - tracking numbers are typically 10-22 characters
|
|
30
|
+
if (trackingNumber.length < 10 || trackingNumber.length > 22) {
|
|
31
|
+
throw new Error('Invalid tracking number length. Must be between 10-22 characters.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate and auto-detect carrier from tracking number
|
|
35
|
+
const extracted = extractTrackingNumbers(trackingNumber);
|
|
36
|
+
|
|
37
|
+
let detectedCarrier = null;
|
|
38
|
+
let validNumber = trackingNumber;
|
|
39
|
+
|
|
40
|
+
if (extracted.length > 0) {
|
|
41
|
+
detectedCarrier = extracted[0].carrier;
|
|
42
|
+
validNumber = extracted[0].trackingNumber;
|
|
43
|
+
console.log(chalk.green(`โ Valid tracking number detected`));
|
|
44
|
+
if (detectedCarrier) {
|
|
45
|
+
console.log(chalk.gray(` Carrier: ${detectedCarrier}`));
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.yellow('โ ๏ธ Could not auto-detect carrier from tracking number'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.gray(` Tracking: ${validNumber}\n`));
|
|
52
|
+
|
|
53
|
+
// Interactive prompts
|
|
54
|
+
const answers = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'description',
|
|
58
|
+
message: 'What is this package?',
|
|
59
|
+
default: options.name || 'Package',
|
|
60
|
+
validate: (input) => input.trim().length > 0 || 'Description is required'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'list',
|
|
64
|
+
name: 'carrier',
|
|
65
|
+
message: 'Select carrier:',
|
|
66
|
+
choices: detectedCarrier
|
|
67
|
+
? [
|
|
68
|
+
{ name: `${detectedCarrier} (detected)`, value: detectedCarrier },
|
|
69
|
+
new inquirer.Separator(),
|
|
70
|
+
...CARRIERS.filter(c => c !== detectedCarrier)
|
|
71
|
+
]
|
|
72
|
+
: CARRIERS,
|
|
73
|
+
default: detectedCarrier
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'input',
|
|
77
|
+
name: 'retailer',
|
|
78
|
+
message: 'Where did you order from? (optional)',
|
|
79
|
+
default: 'Manual'
|
|
80
|
+
}
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// Add the package
|
|
84
|
+
const pkg = upsertPackage({
|
|
85
|
+
trackingNumber: validNumber,
|
|
86
|
+
carrier: answers.carrier,
|
|
87
|
+
retailer: answers.retailer || 'Manual',
|
|
88
|
+
description: answers.description,
|
|
89
|
+
emailId: null,
|
|
90
|
+
shippedDate: Math.floor(Date.now() / 1000), // Use current time as shipped date
|
|
91
|
+
lastEmailDate: Math.floor(Date.now() / 1000),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
console.log(chalk.green('\nโ Package added successfully!'));
|
|
95
|
+
console.log(chalk.gray(` ${answers.description}`));
|
|
96
|
+
console.log(chalk.gray(` ${answers.carrier} ยท ${validNumber}\n`));
|
|
97
|
+
|
|
98
|
+
return pkg;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
addPackage,
|
|
103
|
+
};
|