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,177 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import inquirer from "inquirer";
4
+ import {
5
+ listAgents,
6
+ createAgent,
7
+ updateAgent,
8
+ deleteAgent,
9
+ getAgent,
10
+ setActiveAgent,
11
+ getActiveAgent,
12
+ getConfig,
13
+ AgentConfig,
14
+ } from "../config.js";
15
+
16
+ function printAgent(agent: AgentConfig, isActive: boolean): void {
17
+ const marker = isActive ? chalk.green("● ") : " ";
18
+ console.log(`${marker}${chalk.bold(agent.name)}`);
19
+ console.log(` Max order amount: ${agent.maxOrderAmount != null ? `$${agent.maxOrderAmount}` : chalk.dim("none")}`);
20
+ console.log(` Require confirmation: ${agent.requireConfirmation ? chalk.green("yes") : chalk.yellow("no")}`);
21
+ console.log(` Allowed categories: ${agent.allowedCategories?.length ? agent.allowedCategories.join(", ") : chalk.dim("all")}`);
22
+ console.log(` Blocked categories: ${agent.blockedCategories?.length ? agent.blockedCategories.join(", ") : chalk.dim("none")}`);
23
+ console.log(` Default address: ${agent.defaultAddressId || chalk.dim("not set")}`);
24
+ console.log(` Default payment: ${agent.defaultPaymentMethodId || chalk.dim("not set")}`);
25
+ console.log();
26
+ }
27
+
28
+ export function registerAgentCommands(program: Command): void {
29
+ const agent = program
30
+ .command("agent")
31
+ .description("Manage agents (safety profiles for ordering)");
32
+
33
+ // ── LIST ───────────────────────────────────────────────────────────
34
+ agent
35
+ .command("list")
36
+ .alias("ls")
37
+ .description("List all agents")
38
+ .action(() => {
39
+ const agents = listAgents();
40
+ const active = getConfig().get("activeAgent");
41
+ console.log(chalk.bold("\nAgents:\n"));
42
+ for (const a of agents) {
43
+ printAgent(a, a.name === active);
44
+ }
45
+ console.log(chalk.dim(`Active agent marked with ${chalk.green("●")}`));
46
+ });
47
+
48
+ // ── CREATE ─────────────────────────────────────────────────────────
49
+ agent
50
+ .command("create <name>")
51
+ .description("Create a new agent")
52
+ .option("--max-amount <amount>", "Max order amount", parseFloat)
53
+ .option("--no-confirm", "Don't require confirmation before ordering")
54
+ .action(async (name: string, opts) => {
55
+ try {
56
+ const newAgent = createAgent(name, {
57
+ maxOrderAmount: opts.maxAmount,
58
+ requireConfirmation: opts.confirm !== false,
59
+ });
60
+ console.log(chalk.green(`\n✓ Agent "${newAgent.name}" created.\n`));
61
+ printAgent(newAgent, false);
62
+ } catch (error: any) {
63
+ console.error(chalk.red(`\n✗ ${error.message}`));
64
+ process.exitCode = 1;
65
+ }
66
+ });
67
+
68
+ // ── USE (switch active) ────────────────────────────────────────────
69
+ agent
70
+ .command("use <name>")
71
+ .description("Switch the active agent")
72
+ .action((name: string) => {
73
+ try {
74
+ setActiveAgent(name);
75
+ console.log(chalk.green(`\n✓ Active agent set to "${name}".`));
76
+ } catch (error: any) {
77
+ console.error(chalk.red(`\n✗ ${error.message}`));
78
+ process.exitCode = 1;
79
+ }
80
+ });
81
+
82
+ // ── SHOW ───────────────────────────────────────────────────────────
83
+ agent
84
+ .command("show [name]")
85
+ .description("Show details of an agent (defaults to active)")
86
+ .action((name?: string) => {
87
+ const agentName = name || getConfig().get("activeAgent");
88
+ const a = getAgent(agentName);
89
+ if (!a) {
90
+ console.error(chalk.red(`\n✗ Agent "${agentName}" not found.`));
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ console.log();
95
+ printAgent(a, agentName === getConfig().get("activeAgent"));
96
+ });
97
+
98
+ // ── UPDATE ─────────────────────────────────────────────────────────
99
+ agent
100
+ .command("update [name]")
101
+ .description("Update an agent's settings (interactive)")
102
+ .action(async (name?: string) => {
103
+ const agentName = name || getConfig().get("activeAgent");
104
+ const existing = getAgent(agentName);
105
+ if (!existing) {
106
+ console.error(chalk.red(`\n✗ Agent "${agentName}" not found.`));
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ const answers = await inquirer.prompt([
112
+ {
113
+ type: "number",
114
+ name: "maxOrderAmount",
115
+ message: "Max order amount ($):",
116
+ default: existing.maxOrderAmount,
117
+ },
118
+ {
119
+ type: "confirm",
120
+ name: "requireConfirmation",
121
+ message: "Require confirmation before ordering?",
122
+ default: existing.requireConfirmation,
123
+ },
124
+ {
125
+ type: "input",
126
+ name: "allowedCategories",
127
+ message: "Allowed categories (comma-separated, empty = all):",
128
+ default: existing.allowedCategories?.join(", ") || "",
129
+ },
130
+ {
131
+ type: "input",
132
+ name: "blockedCategories",
133
+ message: "Blocked categories (comma-separated, empty = none):",
134
+ default: existing.blockedCategories?.join(", ") || "",
135
+ },
136
+ ]);
137
+
138
+ const updated = updateAgent(agentName, {
139
+ maxOrderAmount: answers.maxOrderAmount,
140
+ requireConfirmation: answers.requireConfirmation,
141
+ allowedCategories: answers.allowedCategories
142
+ ? answers.allowedCategories.split(",").map((s: string) => s.trim()).filter(Boolean)
143
+ : [],
144
+ blockedCategories: answers.blockedCategories
145
+ ? answers.blockedCategories.split(",").map((s: string) => s.trim()).filter(Boolean)
146
+ : [],
147
+ });
148
+
149
+ console.log(chalk.green(`\n✓ Agent "${agentName}" updated.\n`));
150
+ printAgent(updated, agentName === getConfig().get("activeAgent"));
151
+ });
152
+
153
+ // ── DELETE ─────────────────────────────────────────────────────────
154
+ agent
155
+ .command("delete <name>")
156
+ .alias("rm")
157
+ .description("Delete an agent")
158
+ .action(async (name: string) => {
159
+ try {
160
+ const { confirm } = await inquirer.prompt([
161
+ {
162
+ type: "confirm",
163
+ name: "confirm",
164
+ message: `Delete agent "${name}"? This cannot be undone.`,
165
+ default: false,
166
+ },
167
+ ]);
168
+ if (!confirm) return;
169
+
170
+ deleteAgent(name);
171
+ console.log(chalk.green(`\n✓ Agent "${name}" deleted.`));
172
+ } catch (error: any) {
173
+ console.error(chalk.red(`\n✗ ${error.message}`));
174
+ process.exitCode = 1;
175
+ }
176
+ });
177
+ }
@@ -0,0 +1,122 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import inquirer from "inquirer";
5
+ import { login, register, logout, isLoggedIn, getUserInfo } from "../auth.js";
6
+
7
+ export function registerAuthCommands(program: Command): void {
8
+ // ── LOGIN ─────────────────────────────────────────────────────────
9
+ program
10
+ .command("login")
11
+ .description("Log in to your CLISHOP account")
12
+ .option("-e, --email <email>", "Email address")
13
+ .option("-p, --password <password>", "Password (omit for secure prompt)")
14
+ .action(async (opts) => {
15
+ try {
16
+ if (await isLoggedIn()) {
17
+ const user = await getUserInfo();
18
+ const { confirm } = await inquirer.prompt([
19
+ {
20
+ type: "confirm",
21
+ name: "confirm",
22
+ message: `You are already logged in as ${chalk.cyan(user?.email || "unknown")}. Log in as a different user?`,
23
+ default: false,
24
+ },
25
+ ]);
26
+ if (!confirm) return;
27
+ }
28
+
29
+ let email = opts.email;
30
+ let password = opts.password;
31
+
32
+ if (!email || !password) {
33
+ const answers = await inquirer.prompt([
34
+ ...(!email
35
+ ? [{ type: "input" as const, name: "email", message: "Email:" }]
36
+ : []),
37
+ ...(!password
38
+ ? [{ type: "password" as const, name: "password", message: "Password:", mask: "*" }]
39
+ : []),
40
+ ]);
41
+ email = email || answers.email;
42
+ password = password || answers.password;
43
+ }
44
+
45
+ const spinner = ora("Logging in...").start();
46
+ const user = await login(email, password);
47
+ spinner.succeed(chalk.green(`Logged in as ${chalk.bold(user.name)} (${user.email})`));
48
+ } catch (error: any) {
49
+ const msg = error?.response?.data?.message || error.message;
50
+ console.error(chalk.red(`\n✗ Login failed: ${msg}`));
51
+ process.exitCode = 1;
52
+ }
53
+ });
54
+
55
+ // ── REGISTER ──────────────────────────────────────────────────────
56
+ program
57
+ .command("register")
58
+ .description("Create a new CLISHOP account")
59
+ .action(async () => {
60
+ try {
61
+ const answers = await inquirer.prompt([
62
+ { type: "input", name: "name", message: "Full name:" },
63
+ { type: "input", name: "email", message: "Email:" },
64
+ {
65
+ type: "password",
66
+ name: "password",
67
+ message: "Password:",
68
+ mask: "*",
69
+ },
70
+ {
71
+ type: "password",
72
+ name: "confirmPassword",
73
+ message: "Confirm password:",
74
+ mask: "*",
75
+ },
76
+ ]);
77
+
78
+ if (answers.password !== answers.confirmPassword) {
79
+ console.error(chalk.red("✗ Passwords do not match."));
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const spinner = ora("Creating account...").start();
85
+ const user = await register(answers.email, answers.password, answers.name);
86
+ spinner.succeed(chalk.green(`Account created! Welcome, ${chalk.bold(user.name)}.`));
87
+ } catch (error: any) {
88
+ const msg = error?.response?.data?.message || error.message;
89
+ console.error(chalk.red(`\n✗ Registration failed: ${msg}`));
90
+ process.exitCode = 1;
91
+ }
92
+ });
93
+
94
+ // ── LOGOUT ────────────────────────────────────────────────────────
95
+ program
96
+ .command("logout")
97
+ .description("Log out of your CLISHOP account")
98
+ .action(async () => {
99
+ const spinner = ora("Logging out...").start();
100
+ await logout();
101
+ spinner.succeed(chalk.green("Logged out."));
102
+ });
103
+
104
+ // ── WHOAMI ────────────────────────────────────────────────────────
105
+ program
106
+ .command("whoami")
107
+ .description("Show the currently logged-in user")
108
+ .action(async () => {
109
+ if (!(await isLoggedIn())) {
110
+ console.log(chalk.yellow("Not logged in. Run: clishop login"));
111
+ return;
112
+ }
113
+ const user = await getUserInfo();
114
+ if (user) {
115
+ console.log(chalk.cyan(` Name: ${user.name}`));
116
+ console.log(chalk.cyan(` Email: ${user.email}`));
117
+ console.log(chalk.cyan(` ID: ${user.id}`));
118
+ } else {
119
+ console.log(chalk.yellow("Logged in but user info unavailable."));
120
+ }
121
+ });
122
+ }
@@ -0,0 +1,56 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getConfig } from "../config.js";
4
+
5
+ export function registerConfigCommands(program: Command): void {
6
+ const config = program
7
+ .command("config")
8
+ .description("View and manage CLI configuration");
9
+
10
+ // ── SHOW ───────────────────────────────────────────────────────────
11
+ config
12
+ .command("show")
13
+ .description("Show current configuration")
14
+ .action(() => {
15
+ const cfg = getConfig();
16
+ console.log(chalk.bold("\nCLISHOP Configuration:\n"));
17
+ console.log(` Active agent: ${chalk.cyan(cfg.get("activeAgent"))}`);
18
+ console.log(` Output format: ${chalk.cyan(cfg.get("outputFormat"))}`);
19
+ console.log(` Config path: ${chalk.dim(cfg.path)}`);
20
+ console.log();
21
+ });
22
+
23
+ // ── SET OUTPUT FORMAT ──────────────────────────────────────────────
24
+ config
25
+ .command("set-output <format>")
26
+ .description("Set output format: human or json")
27
+ .action((format: string) => {
28
+ if (format !== "human" && format !== "json") {
29
+ console.error(chalk.red('✗ Format must be "human" or "json".'));
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const cfg = getConfig();
34
+ cfg.set("outputFormat", format);
35
+ console.log(chalk.green(`\n✓ Output format set to "${format}".`));
36
+ });
37
+
38
+ // ── RESET ──────────────────────────────────────────────────────────
39
+ config
40
+ .command("reset")
41
+ .description("Reset all configuration to defaults")
42
+ .action(() => {
43
+ const cfg = getConfig();
44
+ cfg.clear();
45
+ console.log(chalk.green("\n✓ Configuration reset to defaults."));
46
+ });
47
+
48
+ // ── PATH ───────────────────────────────────────────────────────────
49
+ config
50
+ .command("path")
51
+ .description("Show the config file path")
52
+ .action(() => {
53
+ const cfg = getConfig();
54
+ console.log(cfg.path);
55
+ });
56
+ }
@@ -0,0 +1,334 @@
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 Order {
9
+ id: string;
10
+ checkoutId?: string;
11
+ status: "pending" | "confirmed" | "processing" | "shipped" | "delivered" | "cancelled";
12
+ items: OrderItem[];
13
+ totalAmountInCents: number;
14
+ currency: string;
15
+ storeName?: string;
16
+ shippingAddressId: string;
17
+ paymentMethodId: string;
18
+ paymentLabel?: string;
19
+ agent: string;
20
+ externalOrderId?: string | null;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ shipments?: Shipment[];
24
+ }
25
+
26
+ export interface OrderItem {
27
+ productId: string;
28
+ productName: string;
29
+ quantity: number;
30
+ unitPriceInCents: number;
31
+ totalPriceInCents: number;
32
+ }
33
+
34
+ export interface Shipment {
35
+ id: string;
36
+ carrier?: string;
37
+ trackingNumber?: string;
38
+ trackingUrl?: string;
39
+ status: string;
40
+ }
41
+
42
+ function formatPrice(cents: number, currency: string): string {
43
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
44
+ }
45
+
46
+ const STATUS_COLORS: Record<string, (s: string) => string> = {
47
+ pending: chalk.yellow,
48
+ confirmed: chalk.blue,
49
+ processing: chalk.cyan,
50
+ shipped: chalk.magenta,
51
+ delivered: chalk.green,
52
+ cancelled: chalk.red,
53
+ };
54
+
55
+ export function registerOrderCommands(program: Command): void {
56
+ const order = program
57
+ .command("order")
58
+ .description("Place and manage orders");
59
+
60
+ // ── BUY (quick order) ──────────────────────────────────────────────
61
+ program
62
+ .command("buy <productId>")
63
+ .description("Quick-buy a product")
64
+ .option("-q, --quantity <qty>", "Quantity", parseInt, 1)
65
+ .option("--address <id>", "Shipping address ID (uses agent default if omitted)")
66
+ .option("--payment <id>", "Payment method ID (uses agent default if omitted)")
67
+ .option("-y, --yes", "Skip confirmation prompt")
68
+ .action(async (productId: string, opts) => {
69
+ try {
70
+ const agent = getActiveAgent();
71
+ const addressId = opts.address || agent.defaultAddressId;
72
+ const paymentId = opts.payment || agent.defaultPaymentMethodId;
73
+
74
+ if (!addressId) {
75
+ console.error(chalk.red("\n✗ No address set. Add one with: clishop address add"));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ if (!paymentId) {
80
+ console.error(chalk.red("\n✗ No payment method set. Add one with: clishop payment add"));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ // Fetch product info for confirmation — try regular products first, then extended
86
+ const api = getApiClient();
87
+ const prodSpinner = ora("Fetching product info...").start();
88
+ let product: any;
89
+ let isExtended = false;
90
+ try {
91
+ const prodRes = await api.get(`/products/${productId}`);
92
+ product = prodRes.data.product;
93
+ } catch (err: any) {
94
+ if (err?.response?.status === 404) {
95
+ // Try extended (search result) product
96
+ try {
97
+ const extRes = await api.get(`/products/extended/${productId}`);
98
+ product = extRes.data.product;
99
+ isExtended = true;
100
+ } catch {
101
+ prodSpinner.stop();
102
+ console.error(chalk.red(`\n✗ Product ${productId} not found.`));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ } else {
107
+ prodSpinner.stop();
108
+ throw err;
109
+ }
110
+ }
111
+ prodSpinner.stop();
112
+
113
+ // Handoff products are now transparently handled — CLISHOP procures on behalf of the user
114
+ const totalCents = product.priceInCents * opts.quantity;
115
+
116
+ // Safety check: max order amount (local agent config is in dollars)
117
+ if (agent.maxOrderAmount && (totalCents / 100) > agent.maxOrderAmount) {
118
+ console.error(
119
+ chalk.red(
120
+ `\n✗ Order total (${formatPrice(totalCents, product.currency)}) exceeds agent "${agent.name}" limit of $${agent.maxOrderAmount}.`
121
+ )
122
+ );
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+
127
+ // Category check
128
+ if (agent.blockedCategories?.includes(product.category)) {
129
+ console.error(chalk.red(`\n✗ Category "${product.category}" is blocked for agent "${agent.name}".`));
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ if (agent.allowedCategories?.length && !agent.allowedCategories.includes(product.category)) {
134
+ console.error(chalk.red(`\n✗ Category "${product.category}" is not in the allowed list for agent "${agent.name}".`));
135
+ process.exitCode = 1;
136
+ return;
137
+ }
138
+
139
+ // Confirmation
140
+ if (agent.requireConfirmation && !opts.yes) {
141
+ console.log(chalk.bold("\n Order Summary:"));
142
+ console.log(` Product: ${product.name}`);
143
+ console.log(` Store: ${product.vendor || product.storeName || "—"}`);
144
+ console.log(` Quantity: ${opts.quantity}`);
145
+ console.log(` Total: ${chalk.bold(formatPrice(totalCents, product.currency))}`);
146
+ console.log(` Agent: ${agent.name}`);
147
+ console.log();
148
+
149
+ const { confirm } = await inquirer.prompt([
150
+ {
151
+ type: "confirm",
152
+ name: "confirm",
153
+ message: "Place this order?",
154
+ default: false,
155
+ },
156
+ ]);
157
+ if (!confirm) {
158
+ console.log(chalk.yellow("Order cancelled."));
159
+ return;
160
+ }
161
+ }
162
+
163
+ const spinner = ora("Placing order...").start();
164
+ const res = await api.post("/orders", {
165
+ agent: agent.name,
166
+ items: [{ productId, quantity: opts.quantity }],
167
+ shippingAddressId: addressId,
168
+ paymentMethodId: paymentId,
169
+ });
170
+ spinner.succeed(chalk.green(`Order placed! Order ID: ${chalk.bold(res.data.order.id)}`));
171
+ } catch (error) {
172
+ handleApiError(error);
173
+ }
174
+ });
175
+
176
+ // ── LIST ORDERS ────────────────────────────────────────────────────
177
+ order
178
+ .command("list")
179
+ .alias("ls")
180
+ .description("List your orders")
181
+ .option("--status <status>", "Filter by status")
182
+ .option("-p, --page <page>", "Page number", parseInt, 1)
183
+ .option("--json", "Output raw JSON")
184
+ .action(async (opts) => {
185
+ try {
186
+ const spinner = ora("Fetching orders...").start();
187
+ const api = getApiClient();
188
+ const res = await api.get("/orders", {
189
+ params: {
190
+ status: opts.status,
191
+ page: opts.page,
192
+ },
193
+ });
194
+ spinner.stop();
195
+
196
+ const orders: Order[] = res.data.orders;
197
+
198
+ if (opts.json) {
199
+ console.log(JSON.stringify(orders, null, 2));
200
+ return;
201
+ }
202
+
203
+ if (orders.length === 0) {
204
+ console.log(chalk.yellow("\nNo orders found.\n"));
205
+ return;
206
+ }
207
+
208
+ console.log(chalk.bold("\nYour Orders:\n"));
209
+ for (const o of orders) {
210
+ const statusColor = STATUS_COLORS[o.status] || chalk.white;
211
+ const date = new Date(o.createdAt).toLocaleDateString();
212
+ console.log(
213
+ ` ${chalk.bold(o.id)} ${statusColor(o.status.toUpperCase().padEnd(12))} ${formatPrice(o.totalAmountInCents, o.currency)} ${chalk.dim(date)} ${chalk.dim(`agent: ${o.agent}`)}`
214
+ );
215
+ for (const item of o.items) {
216
+ console.log(chalk.dim(` · ${item.productName} × ${item.quantity}`));
217
+ }
218
+ console.log();
219
+ }
220
+ } catch (error) {
221
+ handleApiError(error);
222
+ }
223
+ });
224
+
225
+ // ── ORDER DETAIL ───────────────────────────────────────────────────
226
+ order
227
+ .command("show <id>")
228
+ .description("Show order details")
229
+ .option("--json", "Output raw JSON")
230
+ .action(async (id: string, opts) => {
231
+ try {
232
+ const spinner = ora("Fetching order...").start();
233
+ const api = getApiClient();
234
+ const res = await api.get(`/orders/${id}`);
235
+ spinner.stop();
236
+
237
+ const o: Order = res.data.order;
238
+
239
+ if (opts.json) {
240
+ console.log(JSON.stringify(o, null, 2));
241
+ return;
242
+ }
243
+
244
+ const statusColor = STATUS_COLORS[o.status] || chalk.white;
245
+ console.log();
246
+ console.log(chalk.bold(` Order ${o.id}`));
247
+ console.log(` Status: ${statusColor(o.status.toUpperCase())}`);
248
+ console.log(` Total: ${chalk.bold(formatPrice(o.totalAmountInCents, o.currency))}`);
249
+ if (o.storeName) console.log(` Store: ${o.storeName}`);
250
+ console.log(` Agent: ${o.agent}`);
251
+ if (o.paymentLabel) console.log(` Payment: ${o.paymentLabel}`);
252
+ console.log(` Placed: ${new Date(o.createdAt).toLocaleString()}`);
253
+ console.log(` Updated: ${new Date(o.updatedAt).toLocaleString()}`);
254
+ if (o.externalOrderId) {
255
+ console.log(` eBay Ref: ${chalk.dim(o.externalOrderId)}`);
256
+ }
257
+ if (o.shipments?.length) {
258
+ for (const s of o.shipments) {
259
+ console.log(` Tracking: ${s.trackingNumber || "pending"} ${s.carrier ? `(${s.carrier})` : ""}`);
260
+ if (s.trackingUrl) console.log(` Track: ${chalk.cyan.underline(s.trackingUrl)}`);
261
+ }
262
+ }
263
+ console.log(chalk.bold("\n Items:"));
264
+ for (const item of o.items) {
265
+ console.log(` · ${item.productName} × ${item.quantity} ${formatPrice(item.totalPriceInCents, o.currency)}`);
266
+ }
267
+
268
+ // Fetch live tracking from vendor if order has an external reference
269
+ if (o.externalOrderId && !o.externalOrderId.startsWith("pend_")) {
270
+ try {
271
+ const trackRes = await api.get(`/orders/${id}/tracking`);
272
+ const { tracking } = trackRes.data;
273
+ if (tracking) {
274
+ const ebayStatus = tracking.status || "UNKNOWN";
275
+ const STATUS_MAP: Record<string, string> = {
276
+ PENDING_AVAILABILITY: "Pending",
277
+ PENDING_PAYMENT: "Awaiting payment",
278
+ PAYMENT_PROCESSING: "Payment processing",
279
+ FULFILLMENT_IN_PROGRESS: "Being packed",
280
+ FULFILLED: "Fulfilled",
281
+ CANCELLED: "Cancelled",
282
+ };
283
+ console.log(chalk.bold("\n eBay Status:"));
284
+ console.log(` ${chalk.cyan(STATUS_MAP[ebayStatus] || ebayStatus)}`);
285
+ for (const li of tracking.line_items || []) {
286
+ if (li.tracking_number) {
287
+ console.log(` Tracking: ${chalk.bold(li.tracking_number)}${li.carrier ? ` (${li.carrier})` : ""}`);
288
+ }
289
+ if (li.tracking_url) {
290
+ console.log(` Track: ${chalk.cyan.underline(li.tracking_url)}`);
291
+ }
292
+ if (li.estimated_delivery) {
293
+ const eta = new Date(li.estimated_delivery).toLocaleDateString();
294
+ console.log(` ETA: ${eta}`);
295
+ }
296
+ }
297
+ } else if (trackRes.data.message) {
298
+ console.log(chalk.dim(`\n Vendor: ${trackRes.data.message}`));
299
+ }
300
+ } catch {
301
+ // Tracking fetch failed silently — don't break order display
302
+ }
303
+ }
304
+ console.log();
305
+ } catch (error) {
306
+ handleApiError(error);
307
+ }
308
+ });
309
+
310
+ // ── CANCEL ─────────────────────────────────────────────────────────
311
+ order
312
+ .command("cancel <id>")
313
+ .description("Cancel an order")
314
+ .action(async (id: string) => {
315
+ try {
316
+ const { confirm } = await inquirer.prompt([
317
+ {
318
+ type: "confirm",
319
+ name: "confirm",
320
+ message: `Cancel order ${id}? This may not be reversible.`,
321
+ default: false,
322
+ },
323
+ ]);
324
+ if (!confirm) return;
325
+
326
+ const spinner = ora("Cancelling order...").start();
327
+ const api = getApiClient();
328
+ await api.post(`/orders/${id}/cancel`);
329
+ spinner.succeed(chalk.green("Order cancelled."));
330
+ } catch (error) {
331
+ handleApiError(error);
332
+ }
333
+ });
334
+ }