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.
- package/.cursor/rules/commit-workflow.mdc +42 -0
- package/README.md +333 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3878 -0
- package/package.json +52 -0
- package/src/api.ts +89 -0
- package/src/auth.ts +117 -0
- package/src/commands/address.ts +213 -0
- package/src/commands/advertise.ts +702 -0
- package/src/commands/agent.ts +177 -0
- package/src/commands/auth.ts +122 -0
- package/src/commands/config.ts +56 -0
- package/src/commands/order.ts +334 -0
- package/src/commands/payment.ts +108 -0
- package/src/commands/review.ts +412 -0
- package/src/commands/search.ts +1319 -0
- package/src/commands/setup.ts +644 -0
- package/src/commands/status.ts +131 -0
- package/src/commands/store.ts +302 -0
- package/src/commands/support.ts +264 -0
- package/src/config.ts +127 -0
- package/src/index.ts +80 -0
- package/tsconfig.json +22 -0
|
@@ -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
|
+
}
|