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,131 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { getApiClient, handleApiError } from "../api.js";
5
+ import { isLoggedIn, getUserInfo } from "../auth.js";
6
+ import { getConfig, getActiveAgent } from "../config.js";
7
+
8
+ export function registerStatusCommand(program: Command): void {
9
+ program
10
+ .command("status")
11
+ .description("Show full account overview — user, agents, addresses, payment methods")
12
+ .option("--json", "Output raw JSON")
13
+ .action(async (opts) => {
14
+ try {
15
+ // Check login
16
+ if (!(await isLoggedIn())) {
17
+ console.log(chalk.yellow("\nNot logged in. Run: clishop login\n"));
18
+ return;
19
+ }
20
+
21
+ const spinner = ora("Fetching account overview...").start();
22
+ const api = getApiClient();
23
+ const cfg = getConfig();
24
+ const activeAgentName = cfg.get("activeAgent") || "default";
25
+
26
+ // Fetch all data in parallel
27
+ const [userInfo, agentsRes] = await Promise.all([
28
+ getUserInfo(),
29
+ api.get("/agents"),
30
+ ]);
31
+
32
+ const agents = agentsRes.data.agents || [];
33
+
34
+ // Fetch addresses and payment methods for each agent
35
+ const agentDetails = await Promise.all(
36
+ agents.map(async (agent: any) => {
37
+ const [addressesRes, paymentsRes] = await Promise.all([
38
+ api.get("/addresses", { params: { agent: agent.name } }),
39
+ api.get("/payment-methods", { params: { agent: agent.name } }),
40
+ ]);
41
+ return {
42
+ ...agent,
43
+ addresses: addressesRes.data.addresses || [],
44
+ paymentMethods: paymentsRes.data.paymentMethods || [],
45
+ };
46
+ })
47
+ );
48
+
49
+ spinner.stop();
50
+
51
+ if (opts.json) {
52
+ console.log(JSON.stringify({
53
+ user: userInfo,
54
+ activeAgent: activeAgentName,
55
+ agents: agentDetails,
56
+ }, null, 2));
57
+ return;
58
+ }
59
+
60
+ // ── User ──────────────────────────────────────────────────────────
61
+ console.log(chalk.bold.cyan("\n═══════════════════════════════════════════════════════════"));
62
+ console.log(chalk.bold.cyan(" CLISHOP Account Overview"));
63
+ console.log(chalk.bold.cyan("═══════════════════════════════════════════════════════════\n"));
64
+
65
+ console.log(chalk.bold(" 👤 User"));
66
+ console.log(` Name: ${chalk.white(userInfo?.name || "—")}`);
67
+ console.log(` Email: ${chalk.white(userInfo?.email || "—")}`);
68
+ console.log(` ID: ${chalk.dim(userInfo?.id || "—")}`);
69
+ console.log();
70
+
71
+ // ── Agents ────────────────────────────────────────────────────────
72
+ console.log(chalk.bold(` 🤖 Agents (${agents.length})`));
73
+ if (agentDetails.length === 0) {
74
+ console.log(chalk.yellow(" No agents found."));
75
+ }
76
+
77
+ for (const agent of agentDetails) {
78
+ const isActive = agent.name === activeAgentName;
79
+ const marker = isActive ? chalk.green("● ") : " ";
80
+ const activeLabel = isActive ? chalk.green(" (active)") : "";
81
+ const limit = agent.maxOrderAmountInCents
82
+ ? `$${(agent.maxOrderAmountInCents / 100).toFixed(2)}`
83
+ : "No limit";
84
+ const confirm = agent.requireConfirmation ? "Yes" : "No";
85
+
86
+ console.log();
87
+ console.log(` ${marker}${chalk.bold.white(agent.name)}${activeLabel}`);
88
+ console.log(` ID: ${chalk.dim(agent.id)}`);
89
+ console.log(` Max order: ${chalk.yellow(limit)}`);
90
+ console.log(` Require confirm: ${confirm}`);
91
+
92
+ // Addresses
93
+ console.log();
94
+ console.log(chalk.bold(` 📍 Addresses (${agent.addresses.length})`));
95
+ if (agent.addresses.length === 0) {
96
+ console.log(chalk.dim(" None"));
97
+ } else {
98
+ for (const addr of agent.addresses) {
99
+ const isDefault = addr.id === agent.defaultAddressId;
100
+ const defaultBadge = isDefault ? chalk.green(" ★ default") : "";
101
+ console.log(` • ${chalk.white(addr.label)}${defaultBadge}`);
102
+ console.log(chalk.dim(` ${addr.line1}, ${addr.city}, ${addr.postalCode} ${addr.country}`));
103
+ }
104
+ }
105
+
106
+ // Payment methods
107
+ console.log();
108
+ console.log(chalk.bold(` 💳 Payment Methods (${agent.paymentMethods.length})`));
109
+ if (agent.paymentMethods.length === 0) {
110
+ console.log(chalk.dim(" None — run: clishop payment add"));
111
+ } else {
112
+ for (const pm of agent.paymentMethods) {
113
+ const isDefault = pm.id === agent.defaultPaymentMethodId;
114
+ const defaultBadge = isDefault ? chalk.green(" ★ default") : "";
115
+ const last4 = pm.last4 ? ` •••• ${pm.last4}` : "";
116
+ console.log(` • ${chalk.white(pm.label)}${last4}${defaultBadge}`);
117
+ console.log(chalk.dim(` ${pm.type} via ${pm.provider || "unknown"} (${pm.id})`));
118
+ }
119
+ }
120
+ }
121
+
122
+ console.log();
123
+ console.log(chalk.dim("───────────────────────────────────────────────────────────"));
124
+ console.log(chalk.dim(" Tip: Use --agent <name> to run commands as a specific agent"));
125
+ console.log(chalk.dim(" Tip: Use 'clishop agent switch <name>' to change active agent"));
126
+ console.log();
127
+ } catch (error) {
128
+ handleApiError(error);
129
+ }
130
+ });
131
+ }
@@ -0,0 +1,302 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { getApiClient, handleApiError } from "../api.js";
5
+
6
+ export interface Store {
7
+ id: string;
8
+ name: string;
9
+ slug: string;
10
+ domain?: string;
11
+ description?: string;
12
+ country?: string;
13
+ currency: string;
14
+ status: string;
15
+ verified: boolean;
16
+ rating: number | null;
17
+ logoUrl?: string;
18
+ contactEmail?: string;
19
+ productCount?: number;
20
+ createdAt: string;
21
+ }
22
+
23
+ interface Product {
24
+ id: string;
25
+ name: string;
26
+ description: string;
27
+ priceInCents: number;
28
+ currency: string;
29
+ category: string;
30
+ rating: number;
31
+ reviewCount: number;
32
+ inStock: boolean;
33
+ backorder: boolean;
34
+ brand?: string;
35
+ variant?: string;
36
+ freeShipping: boolean;
37
+ shippingPriceInCents?: number;
38
+ shippingDays?: number;
39
+ freeReturns: boolean;
40
+ returnWindowDays?: number;
41
+ storeVerified: boolean;
42
+ }
43
+
44
+ function formatPrice(cents: number, currency: string): string {
45
+ return new Intl.NumberFormat("en-US", {
46
+ style: "currency",
47
+ currency,
48
+ }).format(cents / 100);
49
+ }
50
+
51
+ function renderStars(rating: number): string {
52
+ const full = Math.floor(rating);
53
+ const half = rating - full >= 0.5 ? 1 : 0;
54
+ const empty = 5 - full - half;
55
+ return "★".repeat(full) + (half ? "½" : "") + "☆".repeat(empty);
56
+ }
57
+
58
+ function deliveryLabel(days: number): string {
59
+ if (days <= 0) return "Same-day";
60
+ if (days === 1) return "Next-day";
61
+ if (days === 2) return "2-day";
62
+ return `${days}-day`;
63
+ }
64
+
65
+ export function registerStoreCommands(program: Command): void {
66
+ const store = program
67
+ .command("store")
68
+ .description("Browse stores and their catalogs");
69
+
70
+ // ── LIST stores ─────────────────────────────────────────────────────
71
+ store
72
+ .command("list")
73
+ .alias("ls")
74
+ .description("List available stores")
75
+ .option("-q, --query <query>", "Search stores by name")
76
+ .option("--verified", "Only show verified stores")
77
+ .option("--min-rating <rating>", "Minimum store rating (0-5)", parseFloat)
78
+ .option("--country <country>", "Filter by country")
79
+ .option("-s, --sort <field>", "Sort by: name, rating, newest, products", "name")
80
+ .option("--order <dir>", "Sort order: asc, desc", "asc")
81
+ .option("-p, --page <page>", "Page number", parseInt, 1)
82
+ .option("-n, --per-page <count>", "Results per page", parseInt, 20)
83
+ .option("--json", "Output raw JSON")
84
+ .action(async (opts) => {
85
+ try {
86
+ const spinner = ora("Fetching stores...").start();
87
+ const api = getApiClient();
88
+ const res = await api.get("/stores", {
89
+ params: {
90
+ q: opts.query,
91
+ verified: opts.verified || undefined,
92
+ minRating: opts.minRating,
93
+ country: opts.country,
94
+ sort: opts.sort,
95
+ order: opts.order,
96
+ page: opts.page,
97
+ pageSize: opts.perPage,
98
+ },
99
+ });
100
+ spinner.stop();
101
+
102
+ const { stores, total, page, pageSize } = res.data;
103
+
104
+ if (opts.json) {
105
+ console.log(JSON.stringify(res.data, null, 2));
106
+ return;
107
+ }
108
+
109
+ if (stores.length === 0) {
110
+ console.log(chalk.yellow("\nNo stores found.\n"));
111
+ return;
112
+ }
113
+
114
+ console.log(chalk.bold(`\nStores — ${total} found (page ${page})\n`));
115
+
116
+ for (const s of stores as Store[]) {
117
+ const badge = s.verified ? chalk.green(" ✓") : "";
118
+ const rating = s.rating != null
119
+ ? chalk.yellow(renderStars(s.rating)) + chalk.dim(` (${s.rating.toFixed(1)})`)
120
+ : chalk.dim("No rating");
121
+ const products = s.productCount != null
122
+ ? chalk.dim(`${s.productCount} products`)
123
+ : "";
124
+ const country = s.country ? chalk.dim(` ${s.country}`) : "";
125
+
126
+ console.log(` ${chalk.bold.cyan(s.name)}${badge} ${chalk.dim(`(${s.slug})`)}`);
127
+ console.log(` ${rating} ${products}${country}`);
128
+ if (s.description) {
129
+ const desc = s.description.length > 100
130
+ ? s.description.slice(0, 100) + "..."
131
+ : s.description;
132
+ console.log(` ${chalk.dim(desc)}`);
133
+ }
134
+ console.log();
135
+ }
136
+
137
+ const totalPages = Math.ceil(total / pageSize);
138
+ if (totalPages > 1) {
139
+ console.log(chalk.dim(` Page ${page} of ${totalPages}. Use --page to navigate.\n`));
140
+ }
141
+ } catch (error) {
142
+ handleApiError(error);
143
+ }
144
+ });
145
+
146
+ // ── STORE INFO ──────────────────────────────────────────────────────
147
+ store
148
+ .command("info <store>")
149
+ .description("View store details (by name, slug, or ID)")
150
+ .option("--json", "Output raw JSON")
151
+ .action(async (storeId: string, opts) => {
152
+ try {
153
+ const spinner = ora("Fetching store info...").start();
154
+ const api = getApiClient();
155
+ const res = await api.get(`/stores/${encodeURIComponent(storeId)}`);
156
+ spinner.stop();
157
+
158
+ const s: Store = res.data.store;
159
+
160
+ if (opts.json) {
161
+ console.log(JSON.stringify(s, null, 2));
162
+ return;
163
+ }
164
+
165
+ const badge = s.verified ? chalk.green(" ✓ Verified") : chalk.dim(" Unverified");
166
+
167
+ console.log();
168
+ console.log(chalk.bold.cyan(` ${s.name}`) + badge);
169
+ console.log(chalk.dim(` ID: ${s.id} | Slug: ${s.slug}`));
170
+ console.log();
171
+
172
+ if (s.description) console.log(` ${s.description}`);
173
+ console.log();
174
+
175
+ if (s.rating != null) {
176
+ console.log(` Rating: ${chalk.yellow(renderStars(s.rating))} ${chalk.dim(`(${s.rating.toFixed(1)})`)}`);
177
+ }
178
+ if (s.productCount != null) console.log(` Products: ${s.productCount}`);
179
+ if (s.country) console.log(` Country: ${s.country}`);
180
+ console.log(` Currency: ${s.currency}`);
181
+ if (s.domain) console.log(` Website: ${chalk.cyan.underline(s.domain)}`);
182
+ if (s.contactEmail) console.log(` Contact: ${s.contactEmail}`);
183
+ console.log();
184
+ console.log(chalk.dim(` Browse catalog: clishop store catalog ${s.slug}`));
185
+ console.log(chalk.dim(` Search products: clishop search "<query>" --store ${s.slug}`));
186
+ console.log();
187
+ } catch (error) {
188
+ handleApiError(error);
189
+ }
190
+ });
191
+
192
+ // ── STORE CATALOG ───────────────────────────────────────────────────
193
+ store
194
+ .command("catalog <store>")
195
+ .description("Browse a store's product catalog (by name, slug, or ID)")
196
+ .option("-q, --query <query>", "Search within the store's products")
197
+ .option("-c, --category <category>", "Filter by category")
198
+ .option("--min-price <price>", "Minimum price (cents)", parseFloat)
199
+ .option("--max-price <price>", "Maximum price (cents)", parseFloat)
200
+ .option("--min-rating <rating>", "Minimum product rating (1-5)", parseFloat)
201
+ .option("--in-stock", "Only show in-stock items")
202
+ .option("--free-shipping", "Only show items with free shipping")
203
+ .option("-s, --sort <field>", "Sort by: price, rating, newest, name", "newest")
204
+ .option("--order <dir>", "Sort order: asc, desc", "desc")
205
+ .option("-p, --page <page>", "Page number", parseInt, 1)
206
+ .option("-n, --per-page <count>", "Results per page", parseInt, 20)
207
+ .option("--json", "Output raw JSON")
208
+ .action(async (storeId: string, opts) => {
209
+ try {
210
+ const spinner = ora(`Fetching catalog for "${storeId}"...`).start();
211
+ const api = getApiClient();
212
+ const res = await api.get(`/stores/${encodeURIComponent(storeId)}/catalog`, {
213
+ params: {
214
+ q: opts.query,
215
+ category: opts.category,
216
+ minPrice: opts.minPrice,
217
+ maxPrice: opts.maxPrice,
218
+ minRating: opts.minRating,
219
+ inStock: opts.inStock || undefined,
220
+ freeShipping: opts.freeShipping || undefined,
221
+ sort: opts.sort,
222
+ order: opts.order,
223
+ page: opts.page,
224
+ pageSize: opts.perPage,
225
+ },
226
+ });
227
+ spinner.stop();
228
+
229
+ const { store: storeInfo, products, total, page, pageSize } = res.data;
230
+
231
+ if (opts.json) {
232
+ console.log(JSON.stringify(res.data, null, 2));
233
+ return;
234
+ }
235
+
236
+ const badge = storeInfo.verified ? chalk.green(" ✓") : "";
237
+ const storeRating = storeInfo.rating != null
238
+ ? chalk.dim(` (${storeInfo.rating.toFixed(1)} ★)`)
239
+ : "";
240
+
241
+ console.log(
242
+ chalk.bold(`\n${storeInfo.name}${badge}${storeRating} — ${total} products (page ${page})\n`)
243
+ );
244
+
245
+ if (products.length === 0) {
246
+ console.log(chalk.yellow(" No products found matching your filters.\n"));
247
+ return;
248
+ }
249
+
250
+ for (const p of products as Product[]) {
251
+ const stock = p.inStock
252
+ ? chalk.green("In Stock")
253
+ : p.backorder
254
+ ? chalk.yellow("Backorder")
255
+ : chalk.red("Out of Stock");
256
+ const price = chalk.bold.white(formatPrice(p.priceInCents, p.currency));
257
+ const stars = chalk.yellow(renderStars(p.rating));
258
+
259
+ const shippingInfo = p.freeShipping
260
+ ? chalk.green("Free Shipping")
261
+ : p.shippingPriceInCents != null
262
+ ? chalk.dim(`+${formatPrice(p.shippingPriceInCents, p.currency)} shipping`)
263
+ : "";
264
+ const deliveryInfo = p.shippingDays != null
265
+ ? chalk.dim(`(${deliveryLabel(p.shippingDays)})`)
266
+ : "";
267
+
268
+ console.log(` ${chalk.bold.cyan(p.name)} ${chalk.dim(`(${p.id})`)}`);
269
+ console.log(
270
+ ` ${price} ${stock} ${stars} ${chalk.dim(`(${p.reviewCount} reviews)`)}` +
271
+ (shippingInfo ? ` ${shippingInfo}` : "") +
272
+ (deliveryInfo ? ` ${deliveryInfo}` : "")
273
+ );
274
+
275
+ const meta: string[] = [];
276
+ if (p.category) meta.push(p.category);
277
+ if (p.brand) meta.push(p.brand);
278
+ if (p.variant) meta.push(p.variant);
279
+ if (meta.length) console.log(` ${chalk.dim(meta.join(" · "))}`);
280
+
281
+ const returnInfo: string[] = [];
282
+ if (p.freeReturns) returnInfo.push("Free Returns");
283
+ if (p.returnWindowDays) returnInfo.push(`${p.returnWindowDays}d return window`);
284
+ if (returnInfo.length) console.log(` ${chalk.dim(returnInfo.join(" · "))}`);
285
+
286
+ const desc = p.description.length > 120
287
+ ? p.description.slice(0, 120) + "..."
288
+ : p.description;
289
+ console.log(` ${desc}`);
290
+ console.log();
291
+ }
292
+
293
+ const totalPages = Math.ceil(total / pageSize);
294
+ if (totalPages > 1) {
295
+ console.log(chalk.dim(` Page ${page} of ${totalPages}. Use --page to navigate.\n`));
296
+ }
297
+ } catch (error) {
298
+ handleApiError(error);
299
+ }
300
+ });
301
+
302
+ }
@@ -0,0 +1,264 @@
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
+ const CATEGORY_CHOICES = [
8
+ { name: "General question", value: "general" },
9
+ { name: "Damaged item", value: "damaged" },
10
+ { name: "Missing item", value: "missing" },
11
+ { name: "Wrong item received", value: "wrong_item" },
12
+ { name: "Refund request", value: "refund" },
13
+ { name: "Shipping issue", value: "shipping" },
14
+ { name: "Other", value: "other" },
15
+ ];
16
+
17
+ const PRIORITY_CHOICES = [
18
+ { name: "Low", value: "low" },
19
+ { name: "Normal", value: "normal" },
20
+ { name: "High", value: "high" },
21
+ { name: "Urgent", value: "urgent" },
22
+ ];
23
+
24
+ const STATUS_COLORS: Record<string, (s: string) => string> = {
25
+ open: chalk.green,
26
+ in_progress: chalk.cyan,
27
+ awaiting_customer: chalk.yellow,
28
+ awaiting_store: chalk.blue,
29
+ resolved: chalk.gray,
30
+ closed: chalk.dim,
31
+ };
32
+
33
+ const PRIORITY_COLORS: Record<string, (s: string) => string> = {
34
+ low: chalk.dim,
35
+ normal: chalk.white,
36
+ high: chalk.yellow,
37
+ urgent: chalk.red,
38
+ };
39
+
40
+ export function registerSupportCommands(program: Command): void {
41
+ const support = program
42
+ .command("support")
43
+ .description("Manage support tickets for orders");
44
+
45
+ // ── CREATE TICKET ───────────────────────────────────────────────────
46
+ support
47
+ .command("create <orderId>")
48
+ .alias("new")
49
+ .description("Create a support ticket for an order")
50
+ .action(async (orderId: string) => {
51
+ try {
52
+ const answers = await inquirer.prompt([
53
+ {
54
+ type: "select",
55
+ name: "category",
56
+ message: "What is this about?",
57
+ choices: CATEGORY_CHOICES,
58
+ },
59
+ {
60
+ type: "select",
61
+ name: "priority",
62
+ message: "Priority:",
63
+ choices: PRIORITY_CHOICES,
64
+ default: "normal",
65
+ },
66
+ {
67
+ type: "input",
68
+ name: "subject",
69
+ message: "Subject (short description):",
70
+ validate: (v: string) => v.trim().length > 0 || "Subject is required",
71
+ },
72
+ {
73
+ type: "editor",
74
+ name: "message",
75
+ message: "Describe the issue in detail (opens editor):",
76
+ },
77
+ ]);
78
+
79
+ if (!answers.message || !answers.message.trim()) {
80
+ console.error(chalk.red("\n✗ Message is required.\n"));
81
+ return;
82
+ }
83
+
84
+ const spinner = ora("Creating support ticket...").start();
85
+ const api = getApiClient();
86
+ const res = await api.post("/support", {
87
+ orderId,
88
+ subject: answers.subject.trim(),
89
+ category: answers.category,
90
+ priority: answers.priority,
91
+ message: answers.message.trim(),
92
+ });
93
+ spinner.succeed(chalk.green("Support ticket created!"));
94
+
95
+ const t = res.data.ticket;
96
+ console.log();
97
+ console.log(` ${chalk.bold("Ticket ID:")} ${chalk.cyan(t.id)}`);
98
+ console.log(` ${chalk.bold("Subject:")} ${t.subject}`);
99
+ console.log(` ${chalk.bold("Store:")} ${t.storeName}`);
100
+ console.log(` ${chalk.bold("Category:")} ${t.category}`);
101
+ console.log(` ${chalk.bold("Status:")} ${(STATUS_COLORS[t.status] || chalk.white)(t.status)}`);
102
+ console.log();
103
+ console.log(chalk.dim(" The store will be notified. You can follow up with:"));
104
+ console.log(chalk.dim(` clishop support show ${t.id}`));
105
+ console.log();
106
+ } catch (error) {
107
+ handleApiError(error);
108
+ }
109
+ });
110
+
111
+ // ── LIST TICKETS ────────────────────────────────────────────────────
112
+ support
113
+ .command("list")
114
+ .alias("ls")
115
+ .description("List your support tickets")
116
+ .option("--status <status>", "Filter by status")
117
+ .option("--json", "Output raw JSON")
118
+ .action(async (opts) => {
119
+ try {
120
+ const spinner = ora("Fetching tickets...").start();
121
+ const api = getApiClient();
122
+ const res = await api.get("/support", {
123
+ params: { status: opts.status },
124
+ });
125
+ spinner.stop();
126
+
127
+ const tickets = res.data.tickets;
128
+
129
+ if (opts.json) {
130
+ console.log(JSON.stringify(tickets, null, 2));
131
+ return;
132
+ }
133
+
134
+ if (tickets.length === 0) {
135
+ console.log(chalk.yellow("\nNo support tickets found.\n"));
136
+ return;
137
+ }
138
+
139
+ console.log(chalk.bold("\nYour Support Tickets:\n"));
140
+ for (const t of tickets) {
141
+ const statusColor = STATUS_COLORS[t.status] || chalk.white;
142
+ const priorityColor = PRIORITY_COLORS[t.priority] || chalk.white;
143
+ const date = new Date(t.createdAt).toLocaleDateString();
144
+
145
+ console.log(
146
+ ` ${chalk.bold(t.id)} ${statusColor(t.status.toUpperCase().padEnd(20))} ${priorityColor(t.priority.padEnd(8))} ${chalk.dim(date)}`
147
+ );
148
+ console.log(` ${chalk.bold(t.subject)}`);
149
+ console.log(` ${chalk.dim(`Store: ${t.storeName} · Order: ${t.orderId} · ${t.category}`)}`);
150
+ if (t.lastMessage) {
151
+ const sender = t.lastMessage.senderType === "store" ? chalk.blue("Store") : t.lastMessage.senderType === "system" ? chalk.gray("System") : chalk.green("You");
152
+ console.log(` ${chalk.dim("Last:")} ${sender}: ${chalk.dim(t.lastMessage.body)}`);
153
+ }
154
+ console.log();
155
+ }
156
+ } catch (error) {
157
+ handleApiError(error);
158
+ }
159
+ });
160
+
161
+ // ── SHOW TICKET ─────────────────────────────────────────────────────
162
+ support
163
+ .command("show <ticketId>")
164
+ .description("View a support ticket and its messages")
165
+ .option("--json", "Output raw JSON")
166
+ .action(async (ticketId: string, opts) => {
167
+ try {
168
+ const spinner = ora("Fetching ticket...").start();
169
+ const api = getApiClient();
170
+ const res = await api.get(`/support/${ticketId}`);
171
+ spinner.stop();
172
+
173
+ const t = res.data.ticket;
174
+
175
+ if (opts.json) {
176
+ console.log(JSON.stringify(t, null, 2));
177
+ return;
178
+ }
179
+
180
+ const statusColor = STATUS_COLORS[t.status] || chalk.white;
181
+ console.log();
182
+ console.log(chalk.bold(` Ticket ${chalk.cyan(t.id)}`));
183
+ console.log(` Subject: ${chalk.bold(t.subject)}`);
184
+ console.log(` Status: ${statusColor(t.status.toUpperCase())}`);
185
+ console.log(` Category: ${t.category}`);
186
+ console.log(` Priority: ${(PRIORITY_COLORS[t.priority] || chalk.white)(t.priority)}`);
187
+ console.log(` Store: ${t.storeName}`);
188
+ console.log(` Order: ${t.orderId}`);
189
+ console.log(` Created: ${new Date(t.createdAt).toLocaleString()}`);
190
+ if (t.resolvedAt) console.log(` Resolved: ${new Date(t.resolvedAt).toLocaleString()}`);
191
+
192
+ console.log(chalk.bold("\n Messages:\n"));
193
+ for (const m of t.messages) {
194
+ const time = new Date(m.createdAt).toLocaleString();
195
+ let sender: string;
196
+ if (m.senderType === "customer") sender = chalk.green.bold("You");
197
+ else if (m.senderType === "store") sender = chalk.blue.bold("Store");
198
+ else sender = chalk.gray.bold("System");
199
+
200
+ console.log(` ${sender} ${chalk.dim(time)}`);
201
+ console.log(` ${m.body}`);
202
+ console.log();
203
+ }
204
+ } catch (error) {
205
+ handleApiError(error);
206
+ }
207
+ });
208
+
209
+ // ── REPLY TO TICKET ─────────────────────────────────────────────────
210
+ support
211
+ .command("reply <ticketId>")
212
+ .description("Reply to a support ticket")
213
+ .action(async (ticketId: string) => {
214
+ try {
215
+ const { message } = await inquirer.prompt([
216
+ {
217
+ type: "editor",
218
+ name: "message",
219
+ message: "Your reply (opens editor):",
220
+ },
221
+ ]);
222
+
223
+ if (!message || !message.trim()) {
224
+ console.error(chalk.red("\n✗ Message is required.\n"));
225
+ return;
226
+ }
227
+
228
+ const spinner = ora("Sending reply...").start();
229
+ const api = getApiClient();
230
+ const res = await api.post(`/support/${ticketId}/reply`, {
231
+ message: message.trim(),
232
+ });
233
+ spinner.succeed(chalk.green("Reply sent!"));
234
+ console.log(chalk.dim(` Ticket status: ${res.data.ticketStatus}\n`));
235
+ } catch (error) {
236
+ handleApiError(error);
237
+ }
238
+ });
239
+
240
+ // ── CLOSE TICKET ────────────────────────────────────────────────────
241
+ support
242
+ .command("close <ticketId>")
243
+ .description("Close a resolved ticket")
244
+ .action(async (ticketId: string) => {
245
+ try {
246
+ const { confirm } = await inquirer.prompt([
247
+ {
248
+ type: "confirm",
249
+ name: "confirm",
250
+ message: "Close this ticket?",
251
+ default: false,
252
+ },
253
+ ]);
254
+ if (!confirm) return;
255
+
256
+ const spinner = ora("Closing ticket...").start();
257
+ const api = getApiClient();
258
+ await api.patch(`/support/${ticketId}/status`, { status: "closed" });
259
+ spinner.succeed(chalk.green("Ticket closed."));
260
+ } catch (error) {
261
+ handleApiError(error);
262
+ }
263
+ });
264
+ }