ebag 0.0.2 → 0.1.1

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/README.md CHANGED
@@ -23,15 +23,17 @@ ebag login --cookie "<cookie>"
23
23
  ```
24
24
 
25
25
  Notes:
26
+
26
27
  - Session data is stored in plain text at `~/.config/ebag/session.json` with owner-only permissions (`0600`).
27
28
  - List-based search caches product details in `~/.config/ebag/cache.json` for up to 6 hours.
28
29
  - Completed orders may be cached locally for faster access.
29
30
 
30
- ## Search
31
+ ## Products
31
32
 
32
33
  ```bash
33
- ebag search "lindt"
34
- ebag search "lindt" --json
34
+ ebag product search "lindt"
35
+ ebag product search "lindt" --json
36
+ ebag product show 5128
35
37
  ```
36
38
 
37
39
  ## Delivery slots
@@ -72,18 +74,32 @@ ebag list show 753250
72
74
  npm run build
73
75
  ```
74
76
 
75
- ### End-to-end tests
77
+ ### Unit tests
76
78
 
77
- By default, `npm run test:e2e` reads cookies from `tests/.secrets/ebag-cookies` (gitignored).
78
- You can also override with `EBAG_COOKIE`.
79
+ ```bash
80
+ npm run test:unit
81
+ ```
79
82
 
80
- Optional query override:
83
+ ### End-to-end tests
84
+
85
+ Requires a valid session in `tests/.config/ebag/session.json` (gitignored):
81
86
 
82
87
  ```bash
83
- EBAG_COOKIE="<cookie>" EBAG_TEST_QUERY="lindt" npm run test:e2e
88
+ npm run test:e2e
84
89
  ```
85
- Store test cookies in `tests/.secrets/ebag-cookies` (gitignored), then run:
90
+
91
+ ### All tests
86
92
 
87
93
  ```bash
88
- npm run test:e2e
94
+ npm test
89
95
  ```
96
+
97
+ ## Changelog
98
+
99
+ ### 0.1.1
100
+
101
+ - Add logging in the config directory
102
+
103
+ ### 0.1.0
104
+
105
+ - Initial release
@@ -1,4 +1,4 @@
1
- import type { OrderDetail, OrderSummary, ProductSummary } from '../lib/types';
1
+ import type { OrderDetail, OrderSummary, ProductSummary } from "../lib/types";
2
2
  export declare function outputJson(data: unknown): void;
3
3
  export declare function formatHeading(text: string): string;
4
4
  export declare function outputProducts(products: ProductSummary[]): void;
@@ -23,84 +23,95 @@ function formatPrice(product) {
23
23
  return product.pricePromo;
24
24
  if (product.price)
25
25
  return product.price;
26
- return '';
26
+ return "";
27
27
  }
28
28
  function outputProducts(products) {
29
29
  for (const product of products) {
30
30
  const price = formatPrice(product);
31
- let source = '';
32
- if (product.source === 'list') {
31
+ let source = "";
32
+ if (product.source === "list") {
33
33
  const listNames = (product.listNames || []).filter(Boolean);
34
34
  if (listNames.length) {
35
- source = ` [${listNames.join(', ')}]`;
35
+ source = ` [${listNames.join(", ")}]`;
36
36
  }
37
37
  }
38
- const line = `${product.id} ${product.name}${price ? ` - ${price}` : ''}${source}`;
38
+ const line = `${product.id} ${product.name}${price ? ` - ${price}` : ""}${source}`;
39
39
  process.stdout.write(`${line}\n`);
40
40
  }
41
41
  }
42
42
  function outputList(items) {
43
43
  for (const item of items) {
44
- const count = item.count !== undefined ? ` (${item.count})` : '';
44
+ const count = item.count !== undefined ? ` (${item.count})` : "";
45
45
  process.stdout.write(`${item.id} ${item.name}${count}\n`);
46
46
  }
47
47
  }
48
48
  function outputProductDetail(data) {
49
- const id = data.id ? String(data.id) : '';
50
- const name = data.name ? String(data.name) : '';
51
- const nameEn = data.name_en ? String(data.name_en) : '';
52
- const brand = typeof data.brand === 'string'
49
+ const id = data.id ? String(data.id) : "";
50
+ const name = data.name ? String(data.name) : "";
51
+ const nameEn = data.name_en ? String(data.name_en) : "";
52
+ const brand = typeof data.brand === "string"
53
53
  ? data.brand
54
54
  : data.brand?.name ||
55
- data.brand
56
- ?.name_bg ||
57
- data.brand
58
- ?.name_en ||
59
- '';
60
- const unit = data.unit_weight_text ? String(data.unit_weight_text) : '';
55
+ data.brand?.name_bg ||
56
+ data.brand?.name_en ||
57
+ "";
58
+ const unit = data.unit_weight_text ? String(data.unit_weight_text) : "";
61
59
  const price = data.current_price_eur ?? data.price_promo_eur ?? data.price_eur;
62
- const priceText = price ? String(price) : '';
63
- const currency = priceText ? 'EUR' : '';
64
- const availability = data.is_available === false ? 'No' : data.is_available === true ? 'Yes' : '';
65
- const urlSlug = data.url_slug ? String(data.url_slug) : '';
66
- const origin = data.country_of_origin ? String(data.country_of_origin) : '';
67
- const expiryRaw = data.expiry_date ? String(data.expiry_date) : '';
60
+ const priceText = price ? String(price) : "";
61
+ const currency = priceText ? "EUR" : "";
62
+ const availability = data.is_available === false
63
+ ? "No"
64
+ : data.is_available === true
65
+ ? "Yes"
66
+ : "";
67
+ const urlSlug = data.url_slug ? String(data.url_slug) : "";
68
+ const origin = data.country_of_origin ? String(data.country_of_origin) : "";
69
+ const expiryRaw = data.expiry_date ? String(data.expiry_date) : "";
68
70
  const expiry = formatDate(expiryRaw);
69
- const description = data.description ? String(data.description) : '';
71
+ const description = data.description ? String(data.description) : "";
70
72
  const kvPairs = [
71
- ['ID', id],
72
- ['Brand', brand],
73
- ['Unit', unit],
74
- ['Price', priceText ? `${priceText}${currency ? ` ${currency}` : ''}` : ''],
75
- ['Available', availability],
76
- ['Origin', origin],
77
- ['Expiry', expiry],
78
- ['Slug', urlSlug],
73
+ ["ID", id],
74
+ ["Brand", brand],
75
+ ["Unit", unit],
76
+ ["Price", priceText ? `${priceText}${currency ? ` ${currency}` : ""}` : ""],
77
+ ["Available", availability],
78
+ ["Origin", origin],
79
+ ["Expiry", expiry],
80
+ ["Slug", urlSlug],
79
81
  ];
80
82
  const energyPairs = formatEnergyValues(data.energy_values);
81
- const kvLines = [
82
- ...kvPairs.filter(([, value]) => value),
83
- ...energyPairs,
84
- ].map(([key, value]) => `${key}: ${value}`);
85
- const headerLines = [`# ${name || 'Product'}`, nameEn ? `*${nameEn}*` : ''].filter(Boolean);
86
- const descriptionMd = description ? htmlToMarkdown(description) : '';
83
+ const kvLines = [...kvPairs.filter(([, value]) => value), ...energyPairs].map(([key, value]) => `${key}: ${value}`);
84
+ const headerLines = [
85
+ `# ${name || "Product"}`,
86
+ nameEn ? `*${nameEn}*` : "",
87
+ ].filter(Boolean);
88
+ const descriptionMd = description ? htmlToMarkdown(description) : "";
87
89
  const { descriptionText, ingredientsText } = splitIngredients(descriptionMd);
88
- const descriptionBlock = descriptionText ? `# Description\n\n${descriptionText}` : '';
89
- const ingredientsBlock = ingredientsText ? `\n# Ingredients\n\n${ingredientsText}` : '';
90
- const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
91
- const output = [headerLines.join('\n'), kvBlock, descriptionBlock, ingredientsBlock]
90
+ const descriptionBlock = descriptionText
91
+ ? `# Description\n\n${descriptionText}`
92
+ : "";
93
+ const ingredientsBlock = ingredientsText
94
+ ? `\n# Ingredients\n\n${ingredientsText}`
95
+ : "";
96
+ const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
97
+ const output = [
98
+ headerLines.join("\n"),
99
+ kvBlock,
100
+ descriptionBlock,
101
+ ingredientsBlock,
102
+ ]
92
103
  .filter(Boolean)
93
- .join('\n');
104
+ .join("\n");
94
105
  process.stdout.write(`${output}\n`);
95
106
  }
96
107
  function formatOrderAmount(order) {
97
- if ('finalAmountEur' in order && order.finalAmountEur) {
108
+ if ("finalAmountEur" in order && order.finalAmountEur) {
98
109
  return `${order.finalAmountEur} EUR`;
99
110
  }
100
- if ('finalAmount' in order && order.finalAmount) {
111
+ if ("finalAmount" in order && order.finalAmount) {
101
112
  return order.finalAmount;
102
113
  }
103
- if ('totals' in order) {
114
+ if ("totals" in order) {
104
115
  if (order.totals.totalPaidEur)
105
116
  return `${order.totals.totalPaidEur} EUR`;
106
117
  if (order.totals.totalEur)
@@ -110,46 +121,46 @@ function formatOrderAmount(order) {
110
121
  if (order.totals.total)
111
122
  return order.totals.total;
112
123
  }
113
- return '';
124
+ return "";
114
125
  }
115
126
  function formatDateInTimeZone(date, timeZone) {
116
- const parts = new Intl.DateTimeFormat('en-US', {
127
+ const parts = new Intl.DateTimeFormat("en-US", {
117
128
  timeZone,
118
- year: 'numeric',
119
- month: '2-digit',
120
- day: '2-digit',
129
+ year: "numeric",
130
+ month: "2-digit",
131
+ day: "2-digit",
121
132
  }).formatToParts(date);
122
133
  const lookup = new Map(parts.map((part) => [part.type, part.value]));
123
- const year = lookup.get('year') || '';
124
- const month = lookup.get('month') || '';
125
- const day = lookup.get('day') || '';
134
+ const year = lookup.get("year") || "";
135
+ const month = lookup.get("month") || "";
136
+ const day = lookup.get("day") || "";
126
137
  return `${year}-${month}-${day}`;
127
138
  }
128
139
  function formatOrderStatus(order) {
129
140
  const status = order.status;
130
141
  if (status === 3)
131
- return 'Отказана';
142
+ return "Отказана";
132
143
  if (status === 4) {
133
- const date = order.shippingDate ? formatDate(order.shippingDate) : '';
134
- const todayDate = formatDateInTimeZone(new Date(), 'Europe/Sofia');
144
+ const date = order.shippingDate ? formatDate(order.shippingDate) : "";
145
+ const todayDate = formatDateInTimeZone(new Date(), "Europe/Sofia");
135
146
  if (date && date >= todayDate)
136
- return 'Нова';
137
- return 'Завършена';
147
+ return "Нова";
148
+ return "Завършена";
138
149
  }
139
150
  if (status !== undefined) {
140
151
  return `Status ${status}`;
141
152
  }
142
- return '';
153
+ return "";
143
154
  }
144
155
  function outputOrdersList(orders) {
145
156
  for (const order of orders) {
146
- const date = order.shippingDate ? formatDate(order.shippingDate) : '';
147
- const slot = order.timeSlotDisplay || '';
157
+ const date = order.shippingDate ? formatDate(order.shippingDate) : "";
158
+ const slot = order.timeSlotDisplay || "";
148
159
  const status = formatOrderStatus(order);
149
160
  const total = formatOrderAmount(order);
150
161
  const parts = [order.id, date, slot].filter(Boolean);
151
- const suffix = [status, total].filter(Boolean).join(' - ');
152
- const line = `${parts.join(' ')}${suffix ? ` - ${suffix}` : ''}`;
162
+ const suffix = [status, total].filter(Boolean).join(" - ");
163
+ const line = `${parts.join(" ")}${suffix ? ` - ${suffix}` : ""}`;
153
164
  process.stdout.write(`${line}\n`);
154
165
  }
155
166
  }
@@ -171,57 +182,67 @@ function outputOrderItems(items) {
171
182
  for (const [group, groupItems] of groupEntries) {
172
183
  process.stdout.write(`## ${group}\n`);
173
184
  for (const item of groupItems) {
174
- const qty = item.quantity ? ` x${item.quantity}` : '';
175
- const unit = item.unit ? ` (${item.unit})` : '';
176
- const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
177
- const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
185
+ const qty = item.quantity ? ` x${item.quantity}` : "";
186
+ const unit = item.unit ? ` (${item.unit})` : "";
187
+ const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
188
+ const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
178
189
  process.stdout.write(`${line}\n`);
179
190
  }
180
- process.stdout.write('\n');
191
+ process.stdout.write("\n");
181
192
  }
182
193
  }
183
194
  if (ungrouped.length) {
184
195
  for (const item of ungrouped) {
185
- const qty = item.quantity ? ` x${item.quantity}` : '';
186
- const unit = item.unit ? ` (${item.unit})` : '';
187
- const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
188
- const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
196
+ const qty = item.quantity ? ` x${item.quantity}` : "";
197
+ const unit = item.unit ? ` (${item.unit})` : "";
198
+ const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
199
+ const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
189
200
  process.stdout.write(`${line}\n`);
190
201
  }
191
- process.stdout.write('\n');
202
+ process.stdout.write("\n");
192
203
  }
193
204
  }
194
205
  function outputOrderDetail(detail) {
195
- const header = `# Order ${detail.id || ''}`.trim();
206
+ const header = `# Order ${detail.id || ""}`.trim();
196
207
  const status = formatOrderStatus(detail);
197
- const date = detail.shippingDate ? formatDate(detail.shippingDate) : '';
198
- const address = detail.address || '';
208
+ const date = detail.shippingDate ? formatDate(detail.shippingDate) : "";
209
+ const address = detail.address || "";
199
210
  const total = formatOrderAmount(detail);
200
211
  const kvPairs = [
201
- ['ID', detail.id],
202
- ['Status', status],
203
- ['Date', date],
204
- ['Timeslot', detail.timeSlotDisplay || ''],
205
- ['Address', address],
206
- ['Total', total],
207
- ['Discount', detail.totals.discountEur ? `${detail.totals.discountEur} EUR` : detail.totals.discount || ''],
208
- ['Tip', detail.totals.tipEur ? `${detail.totals.tipEur} EUR` : detail.totals.tip || ''],
212
+ ["ID", detail.id],
213
+ ["Status", status],
214
+ ["Date", date],
215
+ ["Timeslot", detail.timeSlotDisplay || ""],
216
+ ["Address", address],
217
+ ["Total", total],
218
+ [
219
+ "Discount",
220
+ detail.totals.discountEur
221
+ ? `${detail.totals.discountEur} EUR`
222
+ : detail.totals.discount || "",
223
+ ],
224
+ [
225
+ "Tip",
226
+ detail.totals.tipEur
227
+ ? `${detail.totals.tipEur} EUR`
228
+ : detail.totals.tip || "",
229
+ ],
209
230
  ].filter(([, value]) => value);
210
231
  const kvLines = kvPairs.map(([key, value]) => `${key}: ${value}`);
211
- const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
232
+ const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
212
233
  process.stdout.write(`${header}\n`);
213
234
  if (kvBlock) {
214
235
  process.stdout.write(`${kvBlock}\n`);
215
236
  }
216
- process.stdout.write('# Items\n');
237
+ process.stdout.write("# Items\n");
217
238
  if (detail.items.length) {
218
239
  outputOrderItems(detail.items);
219
240
  }
220
241
  else {
221
- process.stdout.write('No items found.\n\n');
242
+ process.stdout.write("No items found.\n\n");
222
243
  }
223
244
  if (detail.additionalOrders && detail.additionalOrders.length) {
224
- process.stdout.write('# Additional Orders\n');
245
+ process.stdout.write("# Additional Orders\n");
225
246
  for (const additional of detail.additionalOrders) {
226
247
  process.stdout.write(`## Order ${additional.id}\n`);
227
248
  const additionalTotal = formatOrderAmount(additional);
@@ -232,14 +253,14 @@ function outputOrderDetail(detail) {
232
253
  outputOrderItems(additional.items);
233
254
  }
234
255
  else {
235
- process.stdout.write('No items found.\n\n');
256
+ process.stdout.write("No items found.\n\n");
236
257
  }
237
258
  }
238
259
  }
239
260
  }
240
261
  function formatDate(value) {
241
262
  if (!value)
242
- return '';
263
+ return "";
243
264
  const isoMatch = value.match(/(\d{4})-(\d{2})-(\d{2})/);
244
265
  if (isoMatch)
245
266
  return `${isoMatch[1]}-${isoMatch[2]}-${isoMatch[3]}`;
@@ -252,7 +273,7 @@ function formatDate(value) {
252
273
  const parsed = new Date(value);
253
274
  if (Number.isNaN(parsed.getTime()))
254
275
  return value;
255
- const pad = (n) => String(n).padStart(2, '0');
276
+ const pad = (n) => String(n).padStart(2, "0");
256
277
  return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`;
257
278
  }
258
279
  function formatEnergyValues(value) {
@@ -261,7 +282,7 @@ function formatEnergyValues(value) {
261
282
  if (Array.isArray(value)) {
262
283
  const pairs = [];
263
284
  for (const item of value) {
264
- if (!item || typeof item !== 'object')
285
+ if (!item || typeof item !== "object")
265
286
  continue;
266
287
  const entry = item;
267
288
  const label = entry.name_bg ||
@@ -279,7 +300,7 @@ function formatEnergyValues(value) {
279
300
  }
280
301
  return pairs;
281
302
  }
282
- if (typeof value === 'object') {
303
+ if (typeof value === "object") {
283
304
  return Object.entries(value)
284
305
  .filter(([, entry]) => entry !== null && entry !== undefined)
285
306
  .flatMap(([key, entry]) => splitEnergyValues(`Energy ${key}`, String(entry)));
@@ -292,7 +313,7 @@ function splitEnergyValues(label, raw) {
292
313
  const kjMatch = normalized.match(/(\d+(?:\.\d+)?)\s*kJ/i);
293
314
  const hasKcalKjLabel = /kcal/i.test(label) && /kJ/i.test(label);
294
315
  const splitMatch = normalized.match(/^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/);
295
- const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, '').trim();
316
+ const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, "").trim();
296
317
  const energyLabel = baseLabel || label;
297
318
  if (kcalMatch || kjMatch) {
298
319
  const items = [];
@@ -311,40 +332,40 @@ function splitEnergyValues(label, raw) {
311
332
  return [[label, normalized]];
312
333
  }
313
334
  function normalizeNumberString(value) {
314
- if (typeof value === 'number')
335
+ if (typeof value === "number")
315
336
  return value.toString();
316
- if (typeof value !== 'string')
337
+ if (typeof value !== "string")
317
338
  return String(value);
318
- const trimmed = value.trim().replace(/\u00a0/g, ' ');
319
- const noSpaces = trimmed.replace(/\s+/g, '');
320
- if (noSpaces.includes(',')) {
321
- return noSpaces.replace(',', '.');
339
+ const trimmed = value.trim().replace(/\u00a0/g, " ");
340
+ const noSpaces = trimmed.replace(/\s+/g, "");
341
+ if (noSpaces.includes(",")) {
342
+ return noSpaces.replace(",", ".");
322
343
  }
323
344
  return noSpaces;
324
345
  }
325
346
  function htmlToMarkdown(html) {
326
347
  let text = html;
327
- text = text.replace(/<\s*br\s*\/?\s*>/gi, '\n');
328
- text = text.replace(/<\s*\/p\s*>/gi, '\n\n');
329
- text = text.replace(/<\s*p[^>]*>/gi, '');
330
- text = text.replace(/<\s*li[^>]*>/gi, '- ');
331
- text = text.replace(/<\s*\/li\s*>/gi, '\n');
332
- text = text.replace(/<\s*ul[^>]*>/gi, '\n');
333
- text = text.replace(/<\s*\/ul\s*>/gi, '\n');
334
- text = text.replace(/<[^>]+>/g, '');
335
- text = text.replace(/&nbsp;/g, ' ');
336
- text = text.replace(/&amp;/g, '&');
337
- text = text.replace(/&lt;/g, '<');
338
- text = text.replace(/&gt;/g, '>');
339
- text = text.replace(/\n{3,}/g, '\n\n');
348
+ text = text.replace(/<\s*br\s*\/?\s*>/gi, "\n");
349
+ text = text.replace(/<\s*\/p\s*>/gi, "\n\n");
350
+ text = text.replace(/<\s*p[^>]*>/gi, "");
351
+ text = text.replace(/<\s*li[^>]*>/gi, "- ");
352
+ text = text.replace(/<\s*\/li\s*>/gi, "\n");
353
+ text = text.replace(/<\s*ul[^>]*>/gi, "\n");
354
+ text = text.replace(/<\s*\/ul\s*>/gi, "\n");
355
+ text = text.replace(/<[^>]+>/g, "");
356
+ text = text.replace(/&nbsp;/g, " ");
357
+ text = text.replace(/&amp;/g, "&");
358
+ text = text.replace(/&lt;/g, "<");
359
+ text = text.replace(/&gt;/g, ">");
360
+ text = text.replace(/\n{3,}/g, "\n\n");
340
361
  return text.trim();
341
362
  }
342
363
  function splitIngredients(text) {
343
364
  if (!text)
344
- return { descriptionText: '', ingredientsText: '' };
365
+ return { descriptionText: "", ingredientsText: "" };
345
366
  const match = text.match(/(?:^|\n)\s*Съставки\s*:?\s*/i);
346
367
  if (!match || match.index === undefined) {
347
- return { descriptionText: text, ingredientsText: '' };
368
+ return { descriptionText: text, ingredientsText: "" };
348
369
  }
349
370
  const start = match.index;
350
371
  const end = start + match[0].length;