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,108 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { getApiClient, handleApiError } from "../api.js";
5
+ import { getActiveAgent, updateAgent } from "../config.js";
6
+
7
+ export interface PaymentMethod {
8
+ id: string;
9
+ type: "card" | "bank" | "paypal" | "other";
10
+ label: string;
11
+ last4?: string;
12
+ expiresAt?: string;
13
+ }
14
+
15
+ export function registerPaymentCommands(program: Command): void {
16
+ const payment = program
17
+ .command("payment")
18
+ .description("Manage payment methods (scoped to the active agent)");
19
+
20
+ // ── LIST ───────────────────────────────────────────────────────────
21
+ payment
22
+ .command("list")
23
+ .alias("ls")
24
+ .description("List payment methods for the active agent")
25
+ .action(async () => {
26
+ try {
27
+ const agent = getActiveAgent();
28
+ const spinner = ora("Fetching payment methods...").start();
29
+ const api = getApiClient();
30
+ const res = await api.get("/payment-methods", {
31
+ params: { agent: agent.name },
32
+ });
33
+ spinner.stop();
34
+
35
+ const methods: PaymentMethod[] = res.data.paymentMethods;
36
+ if (methods.length === 0) {
37
+ console.log(chalk.yellow("\nNo payment methods found. Add one with: clishop payment add\n"));
38
+ return;
39
+ }
40
+
41
+ console.log(chalk.bold(`\nPayment methods for agent "${agent.name}":\n`));
42
+ for (const pm of methods) {
43
+ const isDefault = pm.id === agent.defaultPaymentMethodId;
44
+ const marker = isDefault ? chalk.green("● ") : " ";
45
+ const last4 = pm.last4 ? ` •••• ${pm.last4}` : "";
46
+ console.log(`${marker}${chalk.bold(pm.label)}${last4} ${chalk.dim(`[${pm.type}]`)} ${chalk.dim(`(${pm.id})`)}`);
47
+ }
48
+ console.log();
49
+ } catch (error) {
50
+ handleApiError(error);
51
+ }
52
+ });
53
+
54
+ // ── ADD ────────────────────────────────────────────────────────────
55
+ payment
56
+ .command("add")
57
+ .description("Add a payment method (opens browser for secure entry)")
58
+ .action(async () => {
59
+ try {
60
+ const agent = getActiveAgent();
61
+ const spinner = ora("Requesting secure payment setup link...").start();
62
+ const api = getApiClient();
63
+ const res = await api.post("/payment-methods/setup", {
64
+ agent: agent.name,
65
+ });
66
+ spinner.stop();
67
+
68
+ const { setupUrl } = res.data;
69
+ console.log(chalk.bold("\nTo add a payment method securely, open this link in your browser:\n"));
70
+ console.log(chalk.cyan.underline(` ${setupUrl}\n`));
71
+ console.log(chalk.dim("The CLI never collects raw card details. Payment is set up via the secure web portal."));
72
+ console.log(chalk.dim("Once completed, run 'clishop payment list' to see your new method.\n"));
73
+ } catch (error) {
74
+ handleApiError(error);
75
+ }
76
+ });
77
+
78
+ // ── REMOVE ─────────────────────────────────────────────────────────
79
+ payment
80
+ .command("remove <id>")
81
+ .alias("rm")
82
+ .description("Remove a payment method")
83
+ .action(async (id: string) => {
84
+ try {
85
+ const spinner = ora("Removing payment method...").start();
86
+ const api = getApiClient();
87
+ await api.delete(`/payment-methods/${id}`);
88
+ spinner.succeed(chalk.green("Payment method removed."));
89
+
90
+ const agent = getActiveAgent();
91
+ if (agent.defaultPaymentMethodId === id) {
92
+ updateAgent(agent.name, { defaultPaymentMethodId: undefined });
93
+ }
94
+ } catch (error) {
95
+ handleApiError(error);
96
+ }
97
+ });
98
+
99
+ // ── SET-DEFAULT ────────────────────────────────────────────────────
100
+ payment
101
+ .command("set-default <id>")
102
+ .description("Set the default payment method for the active agent")
103
+ .action((id: string) => {
104
+ const agent = getActiveAgent();
105
+ updateAgent(agent.name, { defaultPaymentMethodId: id });
106
+ console.log(chalk.green(`\n✓ Default payment for agent "${agent.name}" set to ${id}.`));
107
+ });
108
+ }
@@ -0,0 +1,412 @@
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
+
7
+ export interface ProductReview {
8
+ id: string;
9
+ productId: string;
10
+ productName: string;
11
+ orderId?: string;
12
+ rating: number;
13
+ title: string;
14
+ body: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ export interface StoreReview {
19
+ id: string;
20
+ storeId: string;
21
+ storeName: string;
22
+ orderId?: string;
23
+ rating: number;
24
+ title: string;
25
+ body: string;
26
+ createdAt: string;
27
+ }
28
+
29
+ function renderStars(rating: number): string {
30
+ const filled = Math.round(rating);
31
+ const empty = 10 - filled;
32
+ return chalk.yellow("★".repeat(filled) + "☆".repeat(empty));
33
+ }
34
+
35
+ function renderRating(rating: number): string {
36
+ if (rating === 0) return chalk.dim("No ratings yet");
37
+ const color = rating >= 8 ? chalk.green : rating >= 5 ? chalk.yellow : chalk.red;
38
+ return `${renderStars(rating)} ${color(`${rating.toFixed(1)}/10`)}`;
39
+ }
40
+
41
+ const RATING_CHOICES = [
42
+ { name: "10 ★★★★★★★★★★ Perfect", value: 10 },
43
+ { name: " 9 ★★★★★★★★★☆ Excellent", value: 9 },
44
+ { name: " 8 ★★★★★★★★☆☆ Great", value: 8 },
45
+ { name: " 7 ★★★★★★★☆☆☆ Good", value: 7 },
46
+ { name: " 6 ★★★★★★☆☆☆☆ Above Average", value: 6 },
47
+ { name: " 5 ★★★★★☆☆☆☆☆ Average", value: 5 },
48
+ { name: " 4 ★★★★☆☆☆☆☆☆ Below Average", value: 4 },
49
+ { name: " 3 ★★★☆☆☆☆☆☆☆ Poor", value: 3 },
50
+ { name: " 2 ★★☆☆☆☆☆☆☆☆ Bad", value: 2 },
51
+ { name: " 1 ★☆☆☆☆☆☆☆☆☆ Terrible", value: 1 },
52
+ ];
53
+
54
+ export function registerReviewCommands(program: Command): void {
55
+ const review = program
56
+ .command("review")
57
+ .description("Manage product & store reviews");
58
+
59
+ // ── REVIEW AN ORDER (items + store) ─────────────────────────────────
60
+ review
61
+ .command("order <orderId>")
62
+ .description("Review items and store from an order")
63
+ .action(async (orderId: string) => {
64
+ try {
65
+ const api = getApiClient();
66
+
67
+ // Fetch reviewable items
68
+ const spinner = ora("Fetching order details...").start();
69
+ let reviewable: any;
70
+ try {
71
+ const res = await api.get(`/orders/${orderId}/reviewable`);
72
+ reviewable = res.data;
73
+ } catch (err: any) {
74
+ spinner.stop();
75
+ handleApiError(err);
76
+ return;
77
+ }
78
+ spinner.stop();
79
+
80
+ const unreviewedItems = reviewable.items.filter((i: any) => !i.alreadyReviewed);
81
+ const storeAlreadyReviewed = reviewable.store.alreadyReviewed;
82
+
83
+ if (unreviewedItems.length === 0 && storeAlreadyReviewed) {
84
+ console.log(chalk.yellow("\nYou've already reviewed everything in this order.\n"));
85
+ return;
86
+ }
87
+
88
+ console.log(chalk.bold(`\n Review Order ${chalk.cyan(orderId)}`));
89
+ console.log(chalk.dim(` Store: ${reviewable.store.name}\n`));
90
+
91
+ const itemReviews: any[] = [];
92
+ let storeReview: any = undefined;
93
+
94
+ // ── Review each unreviewed product ──
95
+ for (const item of unreviewedItems) {
96
+ console.log(chalk.bold(` Product: ${item.productName}`));
97
+
98
+ const { wantReview } = await inquirer.prompt([
99
+ {
100
+ type: "confirm",
101
+ name: "wantReview",
102
+ message: `Review "${item.productName}"?`,
103
+ default: true,
104
+ },
105
+ ]);
106
+
107
+ if (!wantReview) continue;
108
+
109
+ const answers = await inquirer.prompt([
110
+ {
111
+ type: "select",
112
+ name: "rating",
113
+ message: "Rating (1-10):",
114
+ choices: RATING_CHOICES,
115
+ },
116
+ {
117
+ type: "input",
118
+ name: "title",
119
+ message: "Review title:",
120
+ validate: (v: string) => v.trim().length > 0 || "Title is required",
121
+ },
122
+ {
123
+ type: "input",
124
+ name: "body",
125
+ message: "Review body:",
126
+ validate: (v: string) => v.trim().length > 0 || "Body is required",
127
+ },
128
+ ]);
129
+
130
+ itemReviews.push({
131
+ productId: item.productId,
132
+ rating: answers.rating,
133
+ title: answers.title.trim(),
134
+ body: answers.body.trim(),
135
+ });
136
+
137
+ console.log(chalk.dim(` ✓ ${item.productName}: ${answers.rating}/10\n`));
138
+ }
139
+
140
+ // ── Review the store ──
141
+ if (!storeAlreadyReviewed) {
142
+ console.log(chalk.bold(` Store: ${reviewable.store.name}`));
143
+
144
+ const { wantStoreReview } = await inquirer.prompt([
145
+ {
146
+ type: "confirm",
147
+ name: "wantStoreReview",
148
+ message: `Review store "${reviewable.store.name}"?`,
149
+ default: true,
150
+ },
151
+ ]);
152
+
153
+ if (wantStoreReview) {
154
+ const storeAnswers = await inquirer.prompt([
155
+ {
156
+ type: "select",
157
+ name: "rating",
158
+ message: "Store rating (1-10):",
159
+ choices: RATING_CHOICES,
160
+ },
161
+ {
162
+ type: "input",
163
+ name: "title",
164
+ message: "Store review title:",
165
+ validate: (v: string) => v.trim().length > 0 || "Title is required",
166
+ },
167
+ {
168
+ type: "input",
169
+ name: "body",
170
+ message: "Store review body:",
171
+ validate: (v: string) => v.trim().length > 0 || "Body is required",
172
+ },
173
+ ]);
174
+
175
+ storeReview = {
176
+ rating: storeAnswers.rating,
177
+ title: storeAnswers.title.trim(),
178
+ body: storeAnswers.body.trim(),
179
+ };
180
+ }
181
+ }
182
+
183
+ if (itemReviews.length === 0 && !storeReview) {
184
+ console.log(chalk.yellow("\nNo reviews submitted.\n"));
185
+ return;
186
+ }
187
+
188
+ // Submit all reviews
189
+ const submitSpinner = ora("Submitting reviews...").start();
190
+ try {
191
+ const res = await api.post(`/orders/${orderId}/reviews`, {
192
+ itemReviews,
193
+ storeReview,
194
+ });
195
+ submitSpinner.succeed(chalk.green("Reviews submitted!"));
196
+
197
+ const data = res.data;
198
+ if (data.productReviews?.length > 0) {
199
+ console.log(chalk.bold("\n Product Reviews:"));
200
+ for (const r of data.productReviews) {
201
+ console.log(` ${renderStars(r.rating)} ${chalk.bold(r.title)}`);
202
+ }
203
+ }
204
+ if (data.storeReview && !data.storeReview.skipped) {
205
+ console.log(chalk.bold("\n Store Review:"));
206
+ console.log(` ${renderStars(data.storeReview.rating)} ${chalk.bold(data.storeReview.title)}`);
207
+ }
208
+ console.log();
209
+ } catch (err) {
210
+ submitSpinner.stop();
211
+ handleApiError(err);
212
+ }
213
+ } catch (error) {
214
+ handleApiError(error);
215
+ }
216
+ });
217
+
218
+ // ── ADD PRODUCT REVIEW ──────────────────────────────────────────────
219
+ review
220
+ .command("add <productId>")
221
+ .description("Write a review for a product")
222
+ .option("--order <orderId>", "Associate with an order")
223
+ .action(async (productId: string, opts) => {
224
+ try {
225
+ const answers = await inquirer.prompt([
226
+ {
227
+ type: "select",
228
+ name: "rating",
229
+ message: "Rating (1-10):",
230
+ choices: RATING_CHOICES,
231
+ },
232
+ { type: "input", name: "title", message: "Review title:", validate: (v: string) => v.trim().length > 0 || "Required" },
233
+ {
234
+ type: "editor",
235
+ name: "body",
236
+ message: "Review body (opens your editor):",
237
+ },
238
+ ]);
239
+
240
+ const spinner = ora("Submitting review...").start();
241
+ const api = getApiClient();
242
+ const res = await api.post(`/products/${productId}/reviews`, {
243
+ rating: answers.rating,
244
+ title: answers.title.trim(),
245
+ body: answers.body.trim(),
246
+ orderId: opts.order || undefined,
247
+ });
248
+ spinner.succeed(chalk.green("Review submitted!"));
249
+
250
+ console.log(`\n ${renderRating(answers.rating)} ${chalk.bold(answers.title)}`);
251
+ console.log(chalk.dim(` Review ID: ${res.data.review.id}\n`));
252
+ } catch (error) {
253
+ handleApiError(error);
254
+ }
255
+ });
256
+
257
+ // ── ADD STORE REVIEW ────────────────────────────────────────────────
258
+ review
259
+ .command("store <storeId>")
260
+ .description("Write a review for a store")
261
+ .option("--order <orderId>", "Associate with an order")
262
+ .action(async (storeId: string, opts) => {
263
+ try {
264
+ const answers = await inquirer.prompt([
265
+ {
266
+ type: "select",
267
+ name: "rating",
268
+ message: "Store rating (1-10):",
269
+ choices: RATING_CHOICES,
270
+ },
271
+ { type: "input", name: "title", message: "Review title:", validate: (v: string) => v.trim().length > 0 || "Required" },
272
+ {
273
+ type: "editor",
274
+ name: "body",
275
+ message: "Review body (opens your editor):",
276
+ },
277
+ ]);
278
+
279
+ const spinner = ora("Submitting store review...").start();
280
+ const api = getApiClient();
281
+ const res = await api.post(`/stores/${storeId}/reviews`, {
282
+ rating: answers.rating,
283
+ title: answers.title.trim(),
284
+ body: answers.body.trim(),
285
+ orderId: opts.order || undefined,
286
+ });
287
+ spinner.succeed(chalk.green("Store review submitted!"));
288
+
289
+ console.log(`\n ${renderRating(answers.rating)} ${chalk.bold(answers.title)}`);
290
+ console.log(chalk.dim(` Review ID: ${res.data.review.id}\n`));
291
+ } catch (error) {
292
+ handleApiError(error);
293
+ }
294
+ });
295
+
296
+ // ── LIST MY REVIEWS ─────────────────────────────────────────────────
297
+ review
298
+ .command("list")
299
+ .alias("ls")
300
+ .description("List your reviews")
301
+ .option("--json", "Output raw JSON")
302
+ .action(async (opts) => {
303
+ try {
304
+ const spinner = ora("Fetching reviews...").start();
305
+ const api = getApiClient();
306
+ const res = await api.get("/reviews/mine");
307
+ spinner.stop();
308
+
309
+ const { productReviews, storeReviews } = res.data;
310
+
311
+ if (opts.json) {
312
+ console.log(JSON.stringify(res.data, null, 2));
313
+ return;
314
+ }
315
+
316
+ if (productReviews.length === 0 && storeReviews.length === 0) {
317
+ console.log(chalk.yellow("\nYou haven't written any reviews yet.\n"));
318
+ return;
319
+ }
320
+
321
+ if (productReviews.length > 0) {
322
+ console.log(chalk.bold("\nProduct Reviews:\n"));
323
+ for (const r of productReviews as ProductReview[]) {
324
+ const date = new Date(r.createdAt).toLocaleDateString();
325
+ console.log(` ${renderStars(r.rating)} ${chalk.bold(r.title)}`);
326
+ console.log(` ${chalk.dim(`on ${r.productName}`)} ${chalk.dim(date)}`);
327
+ console.log(` ${r.body.length > 150 ? r.body.slice(0, 150) + "..." : r.body}`);
328
+ console.log();
329
+ }
330
+ }
331
+
332
+ if (storeReviews.length > 0) {
333
+ console.log(chalk.bold("Store Reviews:\n"));
334
+ for (const r of storeReviews as StoreReview[]) {
335
+ const date = new Date(r.createdAt).toLocaleDateString();
336
+ console.log(` ${renderStars(r.rating)} ${chalk.bold(r.title)}`);
337
+ console.log(` ${chalk.dim(`store: ${r.storeName}`)} ${chalk.dim(date)}`);
338
+ console.log(` ${r.body.length > 150 ? r.body.slice(0, 150) + "..." : r.body}`);
339
+ console.log();
340
+ }
341
+ }
342
+ } catch (error) {
343
+ handleApiError(error);
344
+ }
345
+ });
346
+
347
+ // ── VIEW PRODUCT RATING ─────────────────────────────────────────────
348
+ review
349
+ .command("rating <id>")
350
+ .description("View rating details for a product or store")
351
+ .option("--store", "View store rating instead of product")
352
+ .action(async (id: string, opts) => {
353
+ try {
354
+ const spinner = ora("Fetching rating...").start();
355
+ const api = getApiClient();
356
+ const endpoint = opts.store ? `/stores/${id}/rating` : `/products/${id}/rating`;
357
+ const res = await api.get(endpoint);
358
+ spinner.stop();
359
+
360
+ const { rating } = res.data;
361
+ const entity = opts.store ? res.data.store : res.data.product;
362
+
363
+ console.log();
364
+ console.log(chalk.bold(` ${entity.name}`));
365
+ console.log(` ${renderRating(rating.displayRating)}`);
366
+ console.log();
367
+ console.log(` Reviews: ${rating.reviewCount}`);
368
+ console.log(` Total Orders: ${rating.totalOrders}`);
369
+ console.log(` Bayesian Avg: ${rating.bayesianAverage.toFixed(2)}`);
370
+ console.log(` Effective Cap: ${rating.effectiveCeiling.toFixed(1)}`);
371
+ if (rating.isCapped) {
372
+ console.log(chalk.yellow(` ⚠ Rating is capped — needs more orders to unlock higher rating`));
373
+ if (rating.totalOrders < 100) {
374
+ console.log(chalk.dim(` ${100 - rating.totalOrders} more orders needed to unlock 8.0+ rating`));
375
+ } else if (rating.totalOrders < 1000) {
376
+ console.log(chalk.dim(` ${1000 - rating.totalOrders} more orders needed to unlock 9.0+ rating`));
377
+ }
378
+ }
379
+ console.log();
380
+ } catch (error) {
381
+ handleApiError(error);
382
+ }
383
+ });
384
+
385
+ // ── DELETE REVIEW ───────────────────────────────────────────────────
386
+ review
387
+ .command("delete <reviewId>")
388
+ .alias("rm")
389
+ .description("Delete one of your reviews")
390
+ .option("--store", "Delete a store review")
391
+ .action(async (reviewId: string, opts) => {
392
+ try {
393
+ const { confirm } = await inquirer.prompt([
394
+ {
395
+ type: "confirm",
396
+ name: "confirm",
397
+ message: `Delete review ${reviewId}?`,
398
+ default: false,
399
+ },
400
+ ]);
401
+ if (!confirm) return;
402
+
403
+ const spinner = ora("Deleting review...").start();
404
+ const api = getApiClient();
405
+ const endpoint = opts.store ? `/store-reviews/${reviewId}` : `/reviews/${reviewId}`;
406
+ await api.delete(endpoint);
407
+ spinner.succeed(chalk.green("Review deleted."));
408
+ } catch (error) {
409
+ handleApiError(error);
410
+ }
411
+ });
412
+ }