clishop 0.1.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.
@@ -0,0 +1,702 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import inquirer from "inquirer";
5
+ import { getApiClient, handleApiError } from "../api.js";
6
+ import { getActiveAgent } from "../config.js";
7
+
8
+ export interface AdvertiseRequest {
9
+ id: string;
10
+ status: string;
11
+ title: string;
12
+ description?: string;
13
+ sku?: string;
14
+ brand?: string;
15
+ company?: string;
16
+ features?: string;
17
+ quantity: number;
18
+ recurring: boolean;
19
+ recurringNote?: string;
20
+ bidPriceInCents?: number;
21
+ currency: string;
22
+ speedDays?: number;
23
+ freeReturns?: boolean;
24
+ minReturnDays?: number;
25
+ paymentMethods?: string; // "all" or JSON array of payment method IDs
26
+ address?: {
27
+ id: string;
28
+ label: string;
29
+ line1?: string;
30
+ line2?: string;
31
+ city?: string;
32
+ region?: string;
33
+ postalCode?: string;
34
+ country?: string;
35
+ };
36
+ expiresAt?: string;
37
+ createdAt: string;
38
+ updatedAt: string;
39
+ bids?: AdvertiseBid[];
40
+ }
41
+
42
+ export interface AdvertiseBid {
43
+ id: string;
44
+ storeId: string;
45
+ status: string;
46
+ priceInCents: number;
47
+ currency: string;
48
+ shippingDays?: number;
49
+ freeReturns?: boolean;
50
+ returnWindowDays?: number;
51
+ note?: string;
52
+ store?: {
53
+ id: string;
54
+ name: string;
55
+ slug: string;
56
+ verified: boolean;
57
+ rating: number | null;
58
+ };
59
+ product?: {
60
+ id: string;
61
+ name: string;
62
+ priceInCents: number;
63
+ currency: string;
64
+ };
65
+ createdAt: string;
66
+ }
67
+
68
+ function formatPrice(cents: number, currency: string): string {
69
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
70
+ }
71
+
72
+ const STATUS_COLORS: Record<string, (s: string) => string> = {
73
+ open: chalk.green,
74
+ closed: chalk.dim,
75
+ accepted: chalk.blue,
76
+ cancelled: chalk.red,
77
+ expired: chalk.yellow,
78
+ };
79
+
80
+ const BID_STATUS_COLORS: Record<string, (s: string) => string> = {
81
+ pending: chalk.yellow,
82
+ accepted: chalk.green,
83
+ rejected: chalk.red,
84
+ withdrawn: chalk.dim,
85
+ };
86
+
87
+ export function registerAdvertiseCommands(program: Command): void {
88
+ const advertise = program
89
+ .command("advertise")
90
+ .description("Advertise a request for vendors to bid on (when you can't find what you need)");
91
+
92
+ // ── CREATE (interactive) ────────────────────────────────────────────
93
+ advertise
94
+ .command("create")
95
+ .alias("new")
96
+ .description("Create a new advertised request")
97
+ .action(async () => {
98
+ try {
99
+ const agent = getActiveAgent();
100
+
101
+ console.log(chalk.bold("\n 📢 Advertise a Request\n"));
102
+ console.log(chalk.dim(" Can't find what you need? Describe it and vendors will bid to fulfill it.\n"));
103
+
104
+ const answers = await inquirer.prompt([
105
+ {
106
+ type: "input",
107
+ name: "title",
108
+ message: "What are you looking for? (product name / title):",
109
+ validate: (v: string) => (v.trim() ? true : "Required"),
110
+ },
111
+ {
112
+ type: "input",
113
+ name: "description",
114
+ message: "Describe what you need in detail (optional):",
115
+ },
116
+ {
117
+ type: "input",
118
+ name: "sku",
119
+ message: "Specific SKU (optional):",
120
+ },
121
+ {
122
+ type: "input",
123
+ name: "brand",
124
+ message: "Preferred brand (optional):",
125
+ },
126
+ {
127
+ type: "input",
128
+ name: "company",
129
+ message: "Preferred company / manufacturer (optional):",
130
+ },
131
+ {
132
+ type: "input",
133
+ name: "features",
134
+ message: "Desired features (optional):",
135
+ },
136
+ {
137
+ type: "number",
138
+ name: "quantity",
139
+ message: "Quantity needed:",
140
+ default: 1,
141
+ },
142
+ {
143
+ type: "confirm",
144
+ name: "recurring",
145
+ message: "Is this a recurring order?",
146
+ default: false,
147
+ },
148
+ ]);
149
+
150
+ let recurringNote: string | undefined;
151
+ if (answers.recurring) {
152
+ const recAnswer = await inquirer.prompt([
153
+ {
154
+ type: "input",
155
+ name: "recurringNote",
156
+ message: "How often? (e.g. weekly, monthly, every 2 weeks):",
157
+ },
158
+ ]);
159
+ recurringNote = recAnswer.recurringNote || undefined;
160
+ }
161
+
162
+ const priceAnswers = await inquirer.prompt([
163
+ {
164
+ type: "input",
165
+ name: "bidPrice",
166
+ message: "Max bid price you're willing to pay (e.g. 49.99, or leave empty):",
167
+ },
168
+ ]);
169
+
170
+ let currency = "USD";
171
+ if (priceAnswers.bidPrice && parseFloat(priceAnswers.bidPrice) > 0) {
172
+ const currencyAnswer = await inquirer.prompt([
173
+ {
174
+ type: "list",
175
+ name: "currency",
176
+ message: "Currency:",
177
+ choices: [
178
+ { name: "USD ($)", value: "USD" },
179
+ { name: "EUR (€)", value: "EUR" },
180
+ { name: "GBP (£)", value: "GBP" },
181
+ { name: "CAD (C$)", value: "CAD" },
182
+ { name: "AUD (A$)", value: "AUD" },
183
+ { name: "JPY (¥)", value: "JPY" },
184
+ { name: "CHF (Fr)", value: "CHF" },
185
+ { name: "CNY (¥)", value: "CNY" },
186
+ { name: "INR (₹)", value: "INR" },
187
+ { name: "Other (enter code)", value: "OTHER" },
188
+ ],
189
+ default: "USD",
190
+ },
191
+ ]);
192
+ if (currencyAnswer.currency === "OTHER") {
193
+ const customCurrency = await inquirer.prompt([
194
+ {
195
+ type: "input",
196
+ name: "code",
197
+ message: "Enter 3-letter currency code (e.g. SEK, NZD, MXN):",
198
+ validate: (v: string) => /^[A-Z]{3}$/i.test(v.trim()) || "Enter a valid 3-letter code",
199
+ },
200
+ ]);
201
+ currency = customCurrency.code.toUpperCase().trim();
202
+ } else {
203
+ currency = currencyAnswer.currency;
204
+ }
205
+ }
206
+
207
+ const speedAnswer = await inquirer.prompt([
208
+ {
209
+ type: "input",
210
+ name: "speedDays",
211
+ message: "Desired delivery speed in days (optional):",
212
+ },
213
+ ]);
214
+
215
+ // Return policy preferences
216
+ const returnAnswers = await inquirer.prompt([
217
+ {
218
+ type: "confirm",
219
+ name: "freeReturns",
220
+ message: "Require free returns?",
221
+ default: false,
222
+ },
223
+ {
224
+ type: "input",
225
+ name: "minReturnDays",
226
+ message: "Minimum return window in days (optional, e.g. 30):",
227
+ },
228
+ ]);
229
+
230
+ // Address selection
231
+ const api = getApiClient();
232
+ let addressId: string | undefined;
233
+
234
+ const addrSpinner = ora("Fetching your addresses...").start();
235
+ try {
236
+ const addrRes = await api.get("/addresses", { params: { agent: agent.name } });
237
+ addrSpinner.stop();
238
+ const addresses = addrRes.data.addresses;
239
+
240
+ if (addresses.length > 0) {
241
+ // Build display names and map to IDs
242
+ const addrMap = new Map<string, string>();
243
+ const addrChoices: { name: string; value: string }[] = [];
244
+
245
+ for (const a of addresses) {
246
+ const displayName = `${a.label} — ${a.line1}`;
247
+ addrMap.set(displayName, a.id);
248
+ addrChoices.push({ name: displayName, value: displayName });
249
+ }
250
+ const skipOption = "Skip — don't set a delivery address";
251
+ addrChoices.push({ name: chalk.dim(skipOption), value: skipOption });
252
+
253
+ // Find default display name
254
+ const defaultAddr = addresses.find((a: any) => a.id === agent.defaultAddressId);
255
+ const defaultDisplay = defaultAddr ? `${defaultAddr.label} — ${defaultAddr.line1}` : "";
256
+
257
+ const { selectedAddress } = await inquirer.prompt([
258
+ {
259
+ type: "list",
260
+ name: "selectedAddress",
261
+ message: "Delivery location:",
262
+ choices: addrChoices,
263
+ default: defaultDisplay,
264
+ },
265
+ ]);
266
+
267
+ // Look up the ID from the selected display name
268
+ addressId = addrMap.get(selectedAddress) || undefined;
269
+ } else {
270
+ addrSpinner.stop();
271
+ console.log(chalk.dim(" No addresses found. You can add one later with: clishop address add"));
272
+ }
273
+ } catch {
274
+ addrSpinner.stop();
275
+ console.log(chalk.dim(" Could not fetch addresses. Skipping delivery location."));
276
+ }
277
+
278
+ // Payment method selection
279
+ let paymentMethods: string | undefined;
280
+
281
+ const paySpinner = ora("Fetching your payment methods...").start();
282
+ try {
283
+ const payRes = await api.get("/payment-methods", { params: { agent: agent.name } });
284
+ paySpinner.stop();
285
+ const payments = payRes.data.paymentMethods;
286
+
287
+ if (payments.length > 0) {
288
+ const payChoices = [
289
+ { name: chalk.green("Accept all payment methods"), value: "__ALL__" },
290
+ ...payments.map((p: any) => ({
291
+ name: `${p.label}${p.brand ? ` (${p.brand})` : ""}`,
292
+ value: p.id,
293
+ checked: true, // Default: all user's payment methods selected
294
+ })),
295
+ ];
296
+
297
+ const { selectedPayments } = await inquirer.prompt([
298
+ {
299
+ type: "checkbox",
300
+ name: "selectedPayments",
301
+ message: "Accepted payment methods:",
302
+ choices: payChoices,
303
+ },
304
+ ]);
305
+
306
+ if (selectedPayments.includes("__ALL__")) {
307
+ paymentMethods = "all";
308
+ } else if (selectedPayments.length > 0) {
309
+ paymentMethods = JSON.stringify(selectedPayments);
310
+ }
311
+ } else {
312
+ console.log(chalk.dim(" No payment methods found. You can add one later with: clishop payment add"));
313
+ }
314
+ } catch {
315
+ paySpinner.stop();
316
+ console.log(chalk.dim(" Could not fetch payment methods. Skipping."));
317
+ }
318
+
319
+ // Build the request body
320
+ const body: any = {
321
+ title: answers.title,
322
+ description: answers.description || undefined,
323
+ sku: answers.sku || undefined,
324
+ brand: answers.brand || undefined,
325
+ company: answers.company || undefined,
326
+ features: answers.features || undefined,
327
+ quantity: answers.quantity || 1,
328
+ recurring: answers.recurring,
329
+ recurringNote,
330
+ currency,
331
+ paymentMethods,
332
+ addressId,
333
+ };
334
+
335
+ if (priceAnswers.bidPrice) {
336
+ const price = parseFloat(priceAnswers.bidPrice);
337
+ if (!isNaN(price) && price > 0) {
338
+ body.bidPriceInCents = Math.round(price * 100);
339
+ }
340
+ }
341
+ if (speedAnswer.speedDays) {
342
+ const days = parseInt(speedAnswer.speedDays, 10);
343
+ if (!isNaN(days) && days > 0) {
344
+ body.speedDays = days;
345
+ }
346
+ }
347
+ if (returnAnswers.freeReturns) {
348
+ body.freeReturns = true;
349
+ }
350
+ if (returnAnswers.minReturnDays) {
351
+ const days = parseInt(returnAnswers.minReturnDays, 10);
352
+ if (!isNaN(days) && days > 0) {
353
+ body.minReturnDays = days;
354
+ }
355
+ }
356
+
357
+ const spinner = ora("Publishing your request...").start();
358
+ const res = await api.post("/advertise", body);
359
+ spinner.succeed(chalk.green(`Request published! ID: ${chalk.bold(res.data.advertise.id)}`));
360
+
361
+ console.log(chalk.dim("\n Vendors can now see your request and submit bids."));
362
+ console.log(chalk.dim(` Check bids with: clishop advertise show ${res.data.advertise.id}\n`));
363
+ } catch (error) {
364
+ handleApiError(error);
365
+ }
366
+ });
367
+
368
+ // ── QUICK CREATE (non-interactive, flag-based) ──────────────────────
369
+ advertise
370
+ .command("quick <title>")
371
+ .description("Quickly advertise a request with flags")
372
+ .option("-d, --description <desc>", "Detailed description")
373
+ .option("--sku <sku>", "Specific SKU")
374
+ .option("--brand <brand>", "Preferred brand")
375
+ .option("--company <company>", "Preferred company")
376
+ .option("--features <features>", "Desired features")
377
+ .option("-q, --quantity <qty>", "Quantity", parseInt, 1)
378
+ .option("--recurring", "Recurring order")
379
+ .option("--recurring-note <note>", "Recurrence frequency")
380
+ .option("--bid-price <price>", "Max bid price", parseFloat)
381
+ .option("--currency <code>", "Currency code (default: USD)")
382
+ .option("--speed <days>", "Desired delivery days", parseInt)
383
+ .option("--free-returns", "Require free returns")
384
+ .option("--min-return-days <days>", "Minimum return window in days", parseInt)
385
+ .option("--payment-methods <methods>", 'Payment methods: "all" or comma-separated IDs')
386
+ .option("--address <id>", "Address ID for delivery")
387
+ .action(async (title: string, opts) => {
388
+ try {
389
+ // Process payment methods
390
+ let paymentMethods: string | undefined;
391
+ if (opts.paymentMethods) {
392
+ if (opts.paymentMethods.toLowerCase() === "all") {
393
+ paymentMethods = "all";
394
+ } else {
395
+ // Convert comma-separated IDs to JSON array
396
+ const ids = opts.paymentMethods.split(",").map((id: string) => id.trim()).filter(Boolean);
397
+ if (ids.length > 0) {
398
+ paymentMethods = JSON.stringify(ids);
399
+ }
400
+ }
401
+ }
402
+
403
+ const body: any = {
404
+ title,
405
+ description: opts.description,
406
+ sku: opts.sku,
407
+ brand: opts.brand,
408
+ company: opts.company,
409
+ features: opts.features,
410
+ quantity: opts.quantity,
411
+ recurring: opts.recurring || false,
412
+ recurringNote: opts.recurringNote,
413
+ currency: opts.currency?.toUpperCase() || "USD",
414
+ paymentMethods,
415
+ addressId: opts.address,
416
+ };
417
+
418
+ if (opts.bidPrice) {
419
+ body.bidPriceInCents = Math.round(opts.bidPrice * 100);
420
+ }
421
+ if (opts.speed) {
422
+ body.speedDays = opts.speed;
423
+ }
424
+ if (opts.freeReturns) {
425
+ body.freeReturns = true;
426
+ }
427
+ if (opts.minReturnDays) {
428
+ body.minReturnDays = opts.minReturnDays;
429
+ }
430
+
431
+ const spinner = ora("Publishing your request...").start();
432
+ const api = getApiClient();
433
+ const res = await api.post("/advertise", body);
434
+ spinner.succeed(chalk.green(`Request published! ID: ${chalk.bold(res.data.advertise.id)}`));
435
+ } catch (error) {
436
+ handleApiError(error);
437
+ }
438
+ });
439
+
440
+ // ── LIST ────────────────────────────────────────────────────────────
441
+ advertise
442
+ .command("list")
443
+ .alias("ls")
444
+ .description("List your advertised requests")
445
+ .option("--status <status>", "Filter by status (open, closed, accepted, cancelled, expired)")
446
+ .option("-p, --page <page>", "Page number", parseInt, 1)
447
+ .option("--json", "Output raw JSON")
448
+ .action(async (opts) => {
449
+ try {
450
+ const spinner = ora("Fetching your requests...").start();
451
+ const api = getApiClient();
452
+ const res = await api.get("/advertise", {
453
+ params: { status: opts.status, page: opts.page },
454
+ });
455
+ spinner.stop();
456
+
457
+ const ads: AdvertiseRequest[] = res.data.advertises;
458
+
459
+ if (opts.json) {
460
+ console.log(JSON.stringify(res.data, null, 2));
461
+ return;
462
+ }
463
+
464
+ if (ads.length === 0) {
465
+ console.log(chalk.yellow("\nNo advertised requests found.\n"));
466
+ console.log(chalk.dim(" Create one with: clishop advertise create\n"));
467
+ return;
468
+ }
469
+
470
+ console.log(chalk.bold("\n📢 Your Advertised Requests:\n"));
471
+ for (const ad of ads) {
472
+ const statusColor = STATUS_COLORS[ad.status] || chalk.white;
473
+ const date = new Date(ad.createdAt).toLocaleDateString();
474
+ const bidCount = ad.bids?.length || 0;
475
+ const bidInfo = bidCount > 0
476
+ ? chalk.cyan(` (${bidCount} bid${bidCount > 1 ? "s" : ""})`)
477
+ : chalk.dim(" (no bids yet)");
478
+
479
+ console.log(
480
+ ` ${chalk.bold(ad.id)} ${statusColor(ad.status.toUpperCase().padEnd(10))} ${chalk.bold(ad.title)}${bidInfo} ${chalk.dim(date)}`
481
+ );
482
+
483
+ const meta: string[] = [];
484
+ if (ad.quantity > 1) meta.push(`qty: ${ad.quantity}`);
485
+ if (ad.brand) meta.push(ad.brand);
486
+ if (ad.bidPriceInCents) meta.push(`max: ${formatPrice(ad.bidPriceInCents, ad.currency)}`);
487
+ if (ad.speedDays) meta.push(`${ad.speedDays}-day delivery`);
488
+ if (ad.freeReturns) meta.push("free returns");
489
+ if (ad.minReturnDays) meta.push(`${ad.minReturnDays}d return min`);
490
+ if (ad.recurring) meta.push("recurring");
491
+ if (ad.address) meta.push(`→ ${ad.address.label}`);
492
+ if (meta.length) {
493
+ console.log(` ${chalk.dim(meta.join(" · "))}`);
494
+ }
495
+ console.log();
496
+ }
497
+
498
+ const totalPages = Math.ceil(res.data.total / res.data.pageSize);
499
+ if (totalPages > 1) {
500
+ console.log(chalk.dim(` Page ${res.data.page} of ${totalPages}. Use --page to navigate.\n`));
501
+ }
502
+ } catch (error) {
503
+ handleApiError(error);
504
+ }
505
+ });
506
+
507
+ // ── SHOW (detail + bids) ────────────────────────────────────────────
508
+ advertise
509
+ .command("show <id>")
510
+ .description("View an advertised request and its bids")
511
+ .option("--json", "Output raw JSON")
512
+ .action(async (id: string, opts) => {
513
+ try {
514
+ const spinner = ora("Fetching request...").start();
515
+ const api = getApiClient();
516
+ const res = await api.get(`/advertise/${id}`);
517
+ spinner.stop();
518
+
519
+ const ad: AdvertiseRequest = res.data.advertise;
520
+
521
+ if (opts.json) {
522
+ console.log(JSON.stringify(ad, null, 2));
523
+ return;
524
+ }
525
+
526
+ const statusColor = STATUS_COLORS[ad.status] || chalk.white;
527
+
528
+ console.log();
529
+ console.log(chalk.bold.cyan(` 📢 ${ad.title}`));
530
+ console.log(` ID: ${chalk.dim(ad.id)}`);
531
+ console.log(` Status: ${statusColor(ad.status.toUpperCase())}`);
532
+ if (ad.description) console.log(` Details: ${ad.description}`);
533
+ if (ad.sku) console.log(` SKU: ${ad.sku}`);
534
+ if (ad.brand) console.log(` Brand: ${ad.brand}`);
535
+ if (ad.company) console.log(` Company: ${ad.company}`);
536
+ if (ad.features) console.log(` Features: ${ad.features}`);
537
+ console.log(` Quantity: ${ad.quantity}`);
538
+ if (ad.bidPriceInCents) console.log(` Max Bid: ${chalk.bold(formatPrice(ad.bidPriceInCents, ad.currency))}`);
539
+ if (ad.speedDays) console.log(` Speed: ${ad.speedDays}-day delivery`);
540
+ // Return policy requirements
541
+ const returnReqs: string[] = [];
542
+ if (ad.freeReturns) returnReqs.push(chalk.green("Free Returns required"));
543
+ if (ad.minReturnDays) returnReqs.push(`${ad.minReturnDays}-day return window min`);
544
+ if (returnReqs.length) console.log(` Returns: ${returnReqs.join(" · ")}`);
545
+ if (ad.recurring) console.log(` Recurring: Yes${ad.recurringNote ? ` (${ad.recurringNote})` : ""}`);
546
+ if (ad.address) {
547
+ const a = ad.address;
548
+ console.log(` Deliver to: ${a.label} — ${a.line1 || ""}${a.city ? `, ${a.city}` : ""}${a.region ? `, ${a.region}` : ""} ${a.postalCode || ""}, ${a.country || ""}`);
549
+ }
550
+ if (ad.paymentMethods) {
551
+ if (ad.paymentMethods === "all") {
552
+ console.log(` Payment: ${chalk.green("All methods accepted")}`);
553
+ } else {
554
+ try {
555
+ const ids = JSON.parse(ad.paymentMethods);
556
+ console.log(` Payment: ${ids.length} method${ids.length > 1 ? "s" : ""} configured`);
557
+ } catch {
558
+ console.log(` Payment: ${ad.paymentMethods}`);
559
+ }
560
+ }
561
+ }
562
+ if (ad.expiresAt) console.log(` Expires: ${new Date(ad.expiresAt).toLocaleString()}`);
563
+ console.log(` Created: ${new Date(ad.createdAt).toLocaleString()}`);
564
+
565
+ // Bids
566
+ const bids = ad.bids || [];
567
+ if (bids.length === 0) {
568
+ console.log(chalk.dim("\n No bids yet. Vendors will be able to see your request and submit bids."));
569
+ } else {
570
+ console.log(chalk.bold(`\n Bids (${bids.length}):\n`));
571
+ for (const bid of bids) {
572
+ const bidStatusColor = BID_STATUS_COLORS[bid.status] || chalk.white;
573
+ const storeBadge = bid.store?.verified ? chalk.green(" ✓") : "";
574
+ const storeRating = bid.store?.rating != null ? chalk.dim(` (${bid.store.rating.toFixed(1)}★)`) : "";
575
+
576
+ console.log(` ${chalk.bold(bid.id)} ${bidStatusColor(bid.status.toUpperCase().padEnd(10))} ${chalk.bold(formatPrice(bid.priceInCents, bid.currency))}`);
577
+ console.log(` Store: ${bid.store?.name || bid.storeId}${storeBadge}${storeRating}`);
578
+ if (bid.shippingDays != null) console.log(` Delivery: ${bid.shippingDays}-day`);
579
+ const bidReturns: string[] = [];
580
+ if (bid.freeReturns) bidReturns.push(chalk.green("Free Returns"));
581
+ if (bid.returnWindowDays) bidReturns.push(`${bid.returnWindowDays}-day return window`);
582
+ if (bidReturns.length) console.log(` Returns: ${bidReturns.join(" · ")}`);
583
+ if (bid.note) console.log(` Note: ${bid.note}`);
584
+ if (bid.product) console.log(` Product: ${bid.product.name} (${formatPrice(bid.product.priceInCents, bid.product.currency)})`);
585
+ console.log(` Date: ${new Date(bid.createdAt).toLocaleString()}`);
586
+ console.log();
587
+ }
588
+
589
+ if (ad.status === "open") {
590
+ const pendingBids = bids.filter((b) => b.status === "pending");
591
+ if (pendingBids.length > 0) {
592
+ console.log(chalk.dim(` Accept a bid: clishop advertise accept ${ad.id} <bidId>`));
593
+ console.log(chalk.dim(` Reject a bid: clishop advertise reject ${ad.id} <bidId>\n`));
594
+ }
595
+ }
596
+ }
597
+ console.log();
598
+ } catch (error) {
599
+ handleApiError(error);
600
+ }
601
+ });
602
+
603
+ // ── ACCEPT BID ──────────────────────────────────────────────────────
604
+ advertise
605
+ .command("accept <advertiseId> <bidId>")
606
+ .description("Accept a vendor's bid on your request")
607
+ .action(async (advertiseId: string, bidId: string) => {
608
+ try {
609
+ // Show bid details first
610
+ const api = getApiClient();
611
+ const detailSpinner = ora("Fetching bid details...").start();
612
+ const detailRes = await api.get(`/advertise/${advertiseId}`);
613
+ detailSpinner.stop();
614
+
615
+ const ad: AdvertiseRequest = detailRes.data.advertise;
616
+ const bid = ad.bids?.find((b) => b.id === bidId);
617
+
618
+ if (!bid) {
619
+ console.error(chalk.red(`\n✗ Bid ${bidId} not found on request ${advertiseId}.`));
620
+ process.exitCode = 1;
621
+ return;
622
+ }
623
+
624
+ console.log(chalk.bold("\n Accept this bid?\n"));
625
+ console.log(` Request: ${ad.title}`);
626
+ console.log(` Store: ${bid.store?.name || bid.storeId}${bid.store?.verified ? chalk.green(" ✓") : ""}`);
627
+ console.log(` Price: ${chalk.bold(formatPrice(bid.priceInCents, bid.currency))}`);
628
+ if (bid.shippingDays != null) console.log(` Delivery: ${bid.shippingDays}-day`);
629
+ if (bid.note) console.log(` Note: ${bid.note}`);
630
+ console.log();
631
+
632
+ const { confirm } = await inquirer.prompt([
633
+ {
634
+ type: "confirm",
635
+ name: "confirm",
636
+ message: "Accept this bid? (All other bids will be rejected)",
637
+ default: false,
638
+ },
639
+ ]);
640
+ if (!confirm) {
641
+ console.log(chalk.yellow("Cancelled."));
642
+ return;
643
+ }
644
+
645
+ const spinner = ora("Accepting bid...").start();
646
+ await api.post(`/advertise/${advertiseId}/bids/${bidId}/accept`);
647
+ spinner.succeed(chalk.green("Bid accepted! The vendor will now fulfill your request."));
648
+ } catch (error) {
649
+ handleApiError(error);
650
+ }
651
+ });
652
+
653
+ // ── REJECT BID ──────────────────────────────────────────────────────
654
+ advertise
655
+ .command("reject <advertiseId> <bidId>")
656
+ .description("Reject a vendor's bid")
657
+ .action(async (advertiseId: string, bidId: string) => {
658
+ try {
659
+ const { confirm } = await inquirer.prompt([
660
+ {
661
+ type: "confirm",
662
+ name: "confirm",
663
+ message: `Reject bid ${bidId}?`,
664
+ default: false,
665
+ },
666
+ ]);
667
+ if (!confirm) return;
668
+
669
+ const spinner = ora("Rejecting bid...").start();
670
+ const api = getApiClient();
671
+ await api.post(`/advertise/${advertiseId}/bids/${bidId}/reject`);
672
+ spinner.succeed(chalk.green("Bid rejected."));
673
+ } catch (error) {
674
+ handleApiError(error);
675
+ }
676
+ });
677
+
678
+ // ── CANCEL ──────────────────────────────────────────────────────────
679
+ advertise
680
+ .command("cancel <id>")
681
+ .description("Cancel an advertised request")
682
+ .action(async (id: string) => {
683
+ try {
684
+ const { confirm } = await inquirer.prompt([
685
+ {
686
+ type: "confirm",
687
+ name: "confirm",
688
+ message: `Cancel advertised request ${id}?`,
689
+ default: false,
690
+ },
691
+ ]);
692
+ if (!confirm) return;
693
+
694
+ const spinner = ora("Cancelling request...").start();
695
+ const api = getApiClient();
696
+ await api.post(`/advertise/${id}/cancel`);
697
+ spinner.succeed(chalk.green("Request cancelled."));
698
+ } catch (error) {
699
+ handleApiError(error);
700
+ }
701
+ });
702
+ }