@mpelka/aliorders 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mpelka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @mpelka/aliorders
2
+
3
+ CLI tool to fetch and display your AliExpress order history, including tracking info, by using the internal web API.
4
+
5
+ ## Requirements
6
+
7
+ - **macOS** (cookie extraction uses Chrome's local database)
8
+ - **Google Chrome** with an active AliExpress login
9
+ - **[Bun](https://bun.sh/)** runtime
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ # From npm
15
+ npm install -g @mpelka/aliorders
16
+
17
+ # Or clone and run directly
18
+ git clone https://github.com/mpelka/aliorders.git
19
+ cd aliorders
20
+ bun install
21
+ bun link
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ aliorders # Show active orders (default)
28
+ aliorders -s all # Show all orders including completed
29
+ aliorders -s shipped # Filter by status
30
+ aliorders -s completed # Show completed orders
31
+ aliorders -n 20 # Fetch up to 20 orders
32
+ aliorders -p 2 # Show page 2
33
+ aliorders --json # Output as JSON for scripting/agents
34
+ aliorders --no-details # Skip tracking lookups (faster)
35
+ aliorders statuses # List all status filters
36
+ ```
37
+
38
+ By default, only active orders are shown (awaiting payment, awaiting shipment, in transit). Use `-s all` to include completed and cancelled orders.
39
+
40
+ ### Status filters
41
+
42
+ | Filter | Aliases | Description |
43
+ |-------------|----------------------|------------------------|
44
+ | `all` | | All orders (no filter) |
45
+ | `waitpay` | `paying` | Awaiting payment |
46
+ | `waitsend` | `shipping` | Awaiting shipment |
47
+ | `waitaccept`| `shipped`, `transit` | Shipped / In transit |
48
+ | `finish` | `completed`, `done` | Completed |
49
+ | `close` | `cancelled` | Cancelled / Closed |
50
+
51
+ ### JSON output
52
+
53
+ Use `--json` to get structured output, useful for piping to `jq` or feeding to LLM agents:
54
+
55
+ ```bash
56
+ aliorders --json | jq '.[].status'
57
+ aliorders --json --no-details # Faster, skips tracking API calls
58
+ ```
59
+
60
+ ### Exit codes
61
+
62
+ | Code | Meaning |
63
+ |------|-----------------|
64
+ | `0` | Success |
65
+ | `1` | Error |
66
+ | `2` | No orders found |
67
+
68
+ ## How it works
69
+
70
+ Every run extracts fresh authentication cookies directly from Chrome's local database for `aliexpress.com`. These cookies are used to sign requests to AliExpress's internal API endpoints. Nothing is persisted to disk — cookies are always read fresh from Chrome.
71
+
72
+ ## Troubleshooting
73
+
74
+ **Token expired / empty response:**
75
+ Visit aliexpress.com in Chrome and browse around (this refreshes the auth token), then run again.
76
+
77
+ ## Security
78
+
79
+ Cookies are read directly from Chrome's database and held in memory only for the duration of the command. Nothing is written to disk.
80
+
81
+ ## License
82
+
83
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mpelka/aliorders",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to fetch and display your AliExpress order history using browser cookies",
5
+ "main": "src/cli.ts",
6
+ "bin": {
7
+ "aliorders": "src/cli.ts"
8
+ },
9
+ "files": [
10
+ "src/**/*.ts",
11
+ "!src/**/*.test.ts",
12
+ "LICENSE",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "bun src/cli.ts",
17
+ "test": "bun test",
18
+ "lint": "biome check src/",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": ["aliexpress", "orders", "cli", "tracking"],
22
+ "author": "mpelka",
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "dependencies": {
26
+ "@mpelka/get-cookies": "^1.0.2",
27
+ "commander": "^14.0.3",
28
+ "ky": "^1.14.3"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.4.6",
32
+ "@types/bun": "^1.3.10",
33
+ "@types/node": "^25.4.0",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }
@@ -0,0 +1,216 @@
1
+ import * as crypto from "node:crypto";
2
+ import type { Cookie } from "@mpelka/get-cookies";
3
+ import ky, { type AfterResponseHook, type BeforeRequestHook } from "ky";
4
+ import type {
5
+ AliExpressApiResponse,
6
+ OrderDetailApiResponse,
7
+ OrderDetailRequestData,
8
+ OrderListOptions,
9
+ RequestConfig,
10
+ } from "./types.ts";
11
+
12
+ export class AliExpressClient {
13
+ private kyInstance: typeof ky;
14
+ private cookies: Map<string, string>;
15
+
16
+ private readonly config = {
17
+ tokenCookieName: "_m_h5_tk",
18
+ appKey: "12574478",
19
+ baseUrl: "https://acs.aliexpress.com/h5/",
20
+ endpoints: {
21
+ orderList: "mtop.aliexpress.trade.buyer.order.list/1.0/",
22
+ orderDetail: "mtop.aliexpress.trade.buyer.order.detail/1.0/",
23
+ },
24
+ defaultParams: {
25
+ jsv: "2.5.1",
26
+ v: "1.0",
27
+ post: "1",
28
+ timeout: 15000,
29
+ dataType: "originaljsonp",
30
+ type: "originaljson",
31
+ },
32
+ };
33
+
34
+ constructor(browserCookies: Cookie[]) {
35
+ this.cookies = new Map();
36
+ for (const cookie of browserCookies) {
37
+ this.cookies.set(cookie.name, cookie.value);
38
+ }
39
+
40
+ const beforeRequestHook: BeforeRequestHook = async (request) => {
41
+ const cookieHeader = [...this.cookies.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
42
+ if (cookieHeader) {
43
+ request.headers.set("Cookie", cookieHeader);
44
+ }
45
+ };
46
+
47
+ const afterResponseHook: AfterResponseHook = async (_request, _options, response) => {
48
+ const setCookieHeader = response.headers.get("set-cookie");
49
+ if (setCookieHeader) {
50
+ for (const part of setCookieHeader.split(",")) {
51
+ const match = part.trim().match(/^([^=]+)=([^;]*)/);
52
+ if (match) {
53
+ this.cookies.set(match[1].trim(), match[2].trim());
54
+ }
55
+ }
56
+ }
57
+ };
58
+
59
+ this.kyInstance = ky.create({
60
+ hooks: {
61
+ beforeRequest: [beforeRequestHook],
62
+ afterResponse: [afterResponseHook],
63
+ },
64
+ headers: {
65
+ accept: "application/json, text/plain, */*",
66
+ "accept-language": "en,pl;q=0.9",
67
+ referer: "https://www.aliexpress.com/",
68
+ "sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
69
+ "sec-ch-ua-mobile": "?0",
70
+ "sec-ch-ua-platform": '"macOS"',
71
+ "sec-fetch-dest": "empty",
72
+ "sec-fetch-mode": "cors",
73
+ "sec-fetch-site": "same-site",
74
+ "user-agent":
75
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
76
+ },
77
+ timeout: 15000,
78
+ retry: 0,
79
+ });
80
+ }
81
+
82
+ private parseJsonpResponse(responseText: string, callbackName: string) {
83
+ const trimmed = responseText.trim();
84
+
85
+ if (trimmed.startsWith(`${callbackName}(`) && trimmed.endsWith(")")) {
86
+ const jsonString = trimmed.slice(callbackName.length + 1, -1);
87
+ return JSON.parse(jsonString);
88
+ }
89
+
90
+ return JSON.parse(responseText);
91
+ }
92
+
93
+ private generateSignature(token: string, timestamp: number, appKey: string, dataString: string): string {
94
+ const signString = `${token}&${timestamp}&${appKey}&${dataString}`;
95
+ return crypto.createHash("md5").update(signString).digest("hex");
96
+ }
97
+
98
+ public getTokenInfo(): { expired: boolean; ageHours: number } {
99
+ const tokenValue = this.cookies.get(this.config.tokenCookieName);
100
+ if (!tokenValue) return { expired: true, ageHours: Infinity };
101
+
102
+ const parts = tokenValue.split("_");
103
+ if (parts.length < 2) return { expired: true, ageHours: Infinity };
104
+
105
+ const tokenTimestamp = parseInt(parts[1], 10);
106
+ if (Number.isNaN(tokenTimestamp)) return { expired: true, ageHours: Infinity };
107
+
108
+ const ageHours = (Date.now() - tokenTimestamp) / (1000 * 60 * 60);
109
+ return { expired: ageHours > 24, ageHours };
110
+ }
111
+
112
+ private getToken(): string | null {
113
+ const tokenValue = this.cookies.get(this.config.tokenCookieName);
114
+ if (!tokenValue) return null;
115
+ return tokenValue.split("_")[0];
116
+ }
117
+
118
+ async getOrders({
119
+ pageIndex = 1,
120
+ pageSize = 10,
121
+ statusTab = null,
122
+ country = "PL",
123
+ language = "pl_PL",
124
+ }: OrderListOptions): Promise<AliExpressApiResponse | null> {
125
+ const dataObject = {
126
+ params: {
127
+ data: {
128
+ pc_om_list_body_109702: { fields: { pageIndex, pageSize } },
129
+ pc_om_list_header_action_110846: {
130
+ fields: {
131
+ statusTab: statusTab ?? "all",
132
+ timeOption: "all",
133
+ searchOption: "order",
134
+ searchInput: "",
135
+ },
136
+ },
137
+ },
138
+ },
139
+ shipToCountry: country,
140
+ _lang: language,
141
+ };
142
+ return this.request({
143
+ endpoint: this.config.endpoints.orderList,
144
+ api: "mtop.aliexpress.trade.buyer.order.list",
145
+ data: dataObject,
146
+ method: "POST",
147
+ callbackName: "mtopjsonp1",
148
+ });
149
+ }
150
+
151
+ async getOrderDetails(
152
+ orderId: string,
153
+ { country = "PL", language = "pl_PL" }: OrderListOptions,
154
+ ): Promise<OrderDetailApiResponse | null> {
155
+ const dataObject: OrderDetailRequestData = {
156
+ tradeOrderId: orderId,
157
+ timeZone: "GMT+0200",
158
+ clientPlatform: "pc",
159
+ channel: "tracking",
160
+ shipToCountry: country,
161
+ _lang: language,
162
+ };
163
+ return this.request({
164
+ endpoint: this.config.endpoints.orderDetail,
165
+ api: "mtop.aliexpress.trade.buyer.order.detail",
166
+ data: dataObject,
167
+ callbackName: "mtopjsonp3",
168
+ needLogin: true,
169
+ method: "GET",
170
+ });
171
+ }
172
+
173
+ private async request(config: RequestConfig) {
174
+ const token = this.getToken();
175
+ if (!token) throw new Error("No authentication token found. Are you logged in to AliExpress in Chrome?");
176
+
177
+ const timestamp = Date.now();
178
+ const dataString = JSON.stringify(config.data);
179
+ const signature = this.generateSignature(token, timestamp, this.config.appKey, dataString);
180
+
181
+ const method = config.method || "GET";
182
+ const urlParams = new URLSearchParams({
183
+ jsv: this.config.defaultParams.jsv,
184
+ appKey: this.config.appKey,
185
+ t: timestamp.toString(),
186
+ sign: signature,
187
+ api: config.api,
188
+ v: this.config.defaultParams.v,
189
+ type: this.config.defaultParams.type,
190
+ dataType: this.config.defaultParams.dataType,
191
+ timeout: this.config.defaultParams.timeout.toString(),
192
+ method: method,
193
+ });
194
+
195
+ if (method === "POST") {
196
+ urlParams.set("post", "1");
197
+ urlParams.set("Content-Type", "application/x-www-form-urlencoded");
198
+ }
199
+ if (method === "GET") {
200
+ urlParams.set("callback", config.callbackName);
201
+ urlParams.set("data", dataString);
202
+ }
203
+
204
+ const url = `${this.config.baseUrl}${config.endpoint}?${urlParams.toString()}`;
205
+
206
+ const response =
207
+ method === "POST"
208
+ ? await this.kyInstance.post(url, { body: new URLSearchParams({ data: dataString }) })
209
+ : await this.kyInstance.get(url);
210
+
211
+ const responseText = await response.text();
212
+ if (responseText.startsWith("{")) return JSON.parse(responseText);
213
+
214
+ return this.parseJsonpResponse(responseText, config.callbackName);
215
+ }
216
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env bun
2
+ import * as os from "node:os";
3
+ import type { Cookie } from "@mpelka/get-cookies";
4
+ import { getChromiumCookiesMacOS } from "@mpelka/get-cookies";
5
+ import { Command } from "commander";
6
+ import { AliExpressClient } from "./AliExpressClient.ts";
7
+ import type {
8
+ LogisticPackageBlock,
9
+ OrderComponentData,
10
+ OrderDetailApiResponse,
11
+ OrderStatusBlock,
12
+ TrackingInfo,
13
+ } from "./types.ts";
14
+
15
+ const STATUS_ALIASES: Record<string, string> = {
16
+ paying: "waitpay",
17
+ shipping: "waitsend",
18
+ shipped: "waitaccept",
19
+ transit: "waitaccept",
20
+ completed: "finish",
21
+ done: "finish",
22
+ cancelled: "close",
23
+ canceled: "close",
24
+ all: "all",
25
+ };
26
+
27
+ const STATUS_NAMES: Record<string, string> = {
28
+ all: "All orders (no filter)",
29
+ waitpay: "Awaiting payment",
30
+ waitsend: "Awaiting shipment",
31
+ waitaccept: "Shipped / In transit",
32
+ finish: "Completed",
33
+ close: "Cancelled / Closed",
34
+ };
35
+
36
+ function resolveStatus(input: string): string | null {
37
+ if (input in STATUS_NAMES) return input;
38
+ if (input in STATUS_ALIASES) return STATUS_ALIASES[input];
39
+ return null;
40
+ }
41
+
42
+ function truncate(str: string, max: number): string {
43
+ return str.length > max ? `${str.slice(0, max - 1)}…` : str;
44
+ }
45
+
46
+ function isActiveOrder(order: OrderComponentData): boolean {
47
+ const status = order.fields.statusText?.toLowerCase() ?? "";
48
+ const completedKeywords = ["completed", "finished", "closed", "cancelled", "canceled", "ukończony", "zamknięty"];
49
+ return !completedKeywords.some((kw) => status.includes(kw));
50
+ }
51
+
52
+ interface OrderOutput {
53
+ orderId: string;
54
+ date: string;
55
+ store: string;
56
+ total: string;
57
+ currency: string;
58
+ status: string;
59
+ tracking?: {
60
+ number: string;
61
+ estimatedDelivery?: string;
62
+ history?: { date: string; action: string; address?: string }[];
63
+ };
64
+ items: { title: string; price: string; quantity: number }[];
65
+ }
66
+
67
+ function extractOrderData(order: OrderComponentData, orderDetails: OrderDetailApiResponse | null): OrderOutput {
68
+ const f = order.fields;
69
+
70
+ let status = f.statusText;
71
+ let trackingNumber: string | null = null;
72
+ let estimatedDelivery: string | null = null;
73
+ let trackingHistory: TrackingInfo[] | null = null;
74
+
75
+ if (orderDetails?.data?.data) {
76
+ const statusBlock = Object.values(orderDetails.data.data).find(
77
+ (block) => block.tag === "detail_order_status_block",
78
+ ) as OrderStatusBlock | undefined;
79
+ if (statusBlock?.fields?.title) {
80
+ status = statusBlock.fields.title;
81
+ }
82
+
83
+ const logisticBlock = Object.values(orderDetails.data.data).find(
84
+ (block) => block.tag === "detail_logistic_package_block",
85
+ ) as LogisticPackageBlock | undefined;
86
+ const packages = logisticBlock?.fields?.packageInfoList;
87
+ if (packages && packages.length > 0) {
88
+ trackingNumber = packages[0].trackingNumber;
89
+ estimatedDelivery = packages[0].expectDeliveryDate;
90
+ trackingHistory = packages[0].trackInfoList;
91
+ }
92
+ }
93
+
94
+ const output: OrderOutput = {
95
+ orderId: f.orderId,
96
+ date: f.orderDateText,
97
+ store: f.storeName,
98
+ total: f.totalPriceText,
99
+ currency: f.currencyCode,
100
+ status,
101
+ items: (f.orderLines ?? []).map((item) => ({
102
+ title: item.itemTitle,
103
+ price: item.itemPriceText,
104
+ quantity: item.quantity,
105
+ })),
106
+ };
107
+
108
+ if (trackingNumber) {
109
+ output.tracking = {
110
+ number: trackingNumber,
111
+ estimatedDelivery: estimatedDelivery ?? undefined,
112
+ history: trackingHistory?.slice(0, 5).map((e) => ({
113
+ date: e.date,
114
+ action: e.action.trim(),
115
+ address: e.address || undefined,
116
+ })),
117
+ };
118
+ }
119
+
120
+ return output;
121
+ }
122
+
123
+ function printOrder(data: OrderOutput, index: number, total: number) {
124
+ const header = `[${index + 1}/${total}] ${data.orderId}`;
125
+ console.log(`\n${header}`);
126
+ console.log("-".repeat(header.length));
127
+ console.log(` ${data.date} | ${data.store} | ${data.total} ${data.currency} | ${data.status}`);
128
+
129
+ if (data.tracking) {
130
+ const parts = [` Tracking: ${data.tracking.number}`];
131
+ if (data.tracking.estimatedDelivery) parts.push(`ETA: ${data.tracking.estimatedDelivery}`);
132
+ console.log(parts.join(" | "));
133
+
134
+ if (data.tracking.history) {
135
+ for (const event of data.tracking.history.slice(0, 3)) {
136
+ const location = event.address ? ` (${event.address})` : "";
137
+ console.log(` ${event.date} - ${event.action}${location}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ for (const item of data.items) {
143
+ console.log(` - ${truncate(item.title, 80)} ${item.price} x${item.quantity}`);
144
+ }
145
+ }
146
+
147
+ async function createClient(): Promise<AliExpressClient | null> {
148
+ if (os.platform() !== "darwin") {
149
+ console.error("Cookie extraction is currently macOS-only.");
150
+ return null;
151
+ }
152
+
153
+ try {
154
+ const cookies: Cookie[] = await getChromiumCookiesMacOS("chrome", "Default", "aliexpress.com");
155
+ if (cookies.length === 0) {
156
+ console.error("No AliExpress cookies found in Chrome. Log in to aliexpress.com first.");
157
+ return null;
158
+ }
159
+
160
+ const tokenCookie = cookies.find((c) => c.name === "_m_h5_tk");
161
+ if (!tokenCookie) {
162
+ console.error("Auth cookie '_m_h5_tk' not found. Visit aliexpress.com in Chrome and browse around.");
163
+ return null;
164
+ }
165
+
166
+ return new AliExpressClient(cookies);
167
+ } catch (error) {
168
+ console.error("Failed to get cookies from Chrome:", error);
169
+ return null;
170
+ }
171
+ }
172
+
173
+ interface RunOptions {
174
+ status?: string;
175
+ limit: number;
176
+ page: number;
177
+ details: boolean;
178
+ json: boolean;
179
+ }
180
+
181
+ async function run(options: RunOptions): Promise<number> {
182
+ const client = await createClient();
183
+ if (!client) return 1;
184
+
185
+ const tokenInfo = client.getTokenInfo();
186
+ if (tokenInfo.expired) {
187
+ console.warn(`Warning: Token is ${tokenInfo.ageHours.toFixed(1)}h old and likely expired.`);
188
+ console.warn(" Visit aliexpress.com in Chrome to refresh your session, then retry.");
189
+ }
190
+
191
+ const showAll = options.status === "all";
192
+ const serverFilter = options.status && !showAll ? options.status : null;
193
+
194
+ const ordersRes = await client.getOrders({
195
+ pageIndex: options.page,
196
+ pageSize: options.limit,
197
+ statusTab: serverFilter,
198
+ });
199
+
200
+ if (ordersRes?.ret?.some((r) => r.startsWith("FAIL_") || r.includes("ERROR") || r.includes("SESSION_EXPIRED"))) {
201
+ console.error(`API error: ${ordersRes.ret.join(", ")}`);
202
+ console.error(" Visit aliexpress.com in Chrome to refresh your session, then retry.");
203
+ return 1;
204
+ }
205
+
206
+ const ordersData = ordersRes?.data?.data;
207
+ if (!ordersData) {
208
+ console.error("Failed to retrieve orders. Response was empty or malformed.");
209
+ if (ordersRes) {
210
+ console.error("Response:", JSON.stringify(ordersRes, null, 2));
211
+ }
212
+ return 1;
213
+ }
214
+
215
+ let orders = Object.keys(ordersData)
216
+ .filter((key) => key.startsWith("pc_om_list_order_"))
217
+ .map((key) => ordersData[key])
218
+ .filter((order) => order?.fields) as OrderComponentData[];
219
+
220
+ // Default: show only active orders. -s all or -s <specific> skips this filter.
221
+ if (!showAll && !options.status) {
222
+ orders = orders.filter(isActiveOrder);
223
+ }
224
+
225
+ if (orders.length === 0) {
226
+ if (options.json) {
227
+ console.log(JSON.stringify([]));
228
+ } else {
229
+ const context = options.status
230
+ ? ` with status "${STATUS_NAMES[options.status] ?? options.status}"`
231
+ : " (active only -- use -s all to include completed)";
232
+ console.log(`No orders found${context}.`);
233
+ }
234
+ return 2;
235
+ }
236
+
237
+ const results: OrderOutput[] = [];
238
+
239
+ for (let i = 0; i < orders.length; i++) {
240
+ const order = orders[i];
241
+ let orderDetails: OrderDetailApiResponse | null = null;
242
+
243
+ if (options.details) {
244
+ orderDetails = await client.getOrderDetails(order.fields.orderId, {});
245
+ if (i < orders.length - 1) {
246
+ await new Promise((resolve) => setTimeout(resolve, 2000));
247
+ }
248
+ }
249
+
250
+ const data = extractOrderData(order, orderDetails);
251
+ results.push(data);
252
+
253
+ if (!options.json) {
254
+ printOrder(data, i, orders.length);
255
+ }
256
+ }
257
+
258
+ if (options.json) {
259
+ console.log(JSON.stringify(results, null, 2));
260
+ } else {
261
+ const label = options.status ? ` (${STATUS_NAMES[options.status] ?? options.status})` : " (active)";
262
+ console.log(`\n${results.length} order${results.length === 1 ? "" : "s"}${label}, page ${options.page}`);
263
+ }
264
+
265
+ return 0;
266
+ }
267
+
268
+ const allStatuses = [...new Set([...Object.keys(STATUS_NAMES), ...Object.keys(STATUS_ALIASES)])].sort();
269
+
270
+ const program = new Command()
271
+ .name("aliorders")
272
+ .description("Fetch and display AliExpress order history from your browser session")
273
+ .version("1.0.0")
274
+ .option("-s, --status <name>", `Filter by status: ${allStatuses.join(", ")}`)
275
+ .option("-n, --limit <number>", "Number of orders to fetch per page", "10")
276
+ .option("-p, --page <number>", "Page number", "1")
277
+ .option("-j, --json", "Output as JSON (useful for scripting and LLM agents)", false)
278
+ .option("--no-details", "Skip fetching detailed tracking info (faster)")
279
+ .addHelpText(
280
+ "after",
281
+ `
282
+ Examples:
283
+ aliorders Show active orders (default)
284
+ aliorders -s all Show all orders including completed
285
+ aliorders -s shipped Show shipped/in-transit orders
286
+ aliorders -s completed Show completed orders
287
+ aliorders -n 20 Fetch up to 20 orders
288
+ aliorders -p 2 Show page 2
289
+ aliorders --json Output as JSON for scripting/agents
290
+ aliorders --no-details Skip tracking lookups (faster)
291
+ aliorders statuses List all status filters`,
292
+ )
293
+ .action(async (opts) => {
294
+ if (opts.status) {
295
+ const resolved = resolveStatus(opts.status);
296
+ if (!resolved) {
297
+ console.error(`Unknown status "${opts.status}". Available: ${allStatuses.join(", ")}`);
298
+ process.exit(1);
299
+ }
300
+ opts.status = resolved;
301
+ }
302
+
303
+ const exitCode = await run({
304
+ status: opts.status,
305
+ limit: parseInt(opts.limit, 10),
306
+ page: parseInt(opts.page, 10),
307
+ details: opts.details,
308
+ json: opts.json,
309
+ });
310
+ process.exit(exitCode);
311
+ });
312
+
313
+ program
314
+ .command("statuses")
315
+ .description("List available order status filters")
316
+ .action(() => {
317
+ console.log("Available status filters:\n");
318
+ for (const [tab, name] of Object.entries(STATUS_NAMES)) {
319
+ const aliases = Object.entries(STATUS_ALIASES)
320
+ .filter(([, v]) => v === tab)
321
+ .map(([k]) => k);
322
+ const aliasStr = aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : "";
323
+ console.log(` ${tab.padEnd(14)} ${name}${aliasStr}`);
324
+ }
325
+ });
326
+
327
+ program.parse();
package/src/types.ts ADDED
@@ -0,0 +1,127 @@
1
+ // Main API response wrapper
2
+ export interface AliExpressApiResponse {
3
+ api: string;
4
+ data: AliExpressResponseData;
5
+ ret: string[];
6
+ v: string;
7
+ }
8
+
9
+ // Container for all response data
10
+ export interface AliExpressResponseData {
11
+ data: Record<string, ComponentData>;
12
+ }
13
+
14
+ // Generic component data structure
15
+ export interface ComponentData {
16
+ fields: any;
17
+ id: string;
18
+ tag: string;
19
+ type: string;
20
+ }
21
+
22
+ // Order-specific component data
23
+ export interface OrderComponentData extends ComponentData {
24
+ fields: OrderFields;
25
+ tag: "pc_om_list_order";
26
+ type: "pc_om_list_order";
27
+ }
28
+
29
+ // Order fields containing all order details
30
+ export interface OrderFields {
31
+ buttons: OrderButton[];
32
+ currencyCode: string;
33
+ orderDateText: string;
34
+ orderId: string;
35
+ orderLines: OrderLine[];
36
+ statusText: string;
37
+ storeName: string;
38
+ totalPriceText: string;
39
+ }
40
+
41
+ // Individual order line (product)
42
+ export interface OrderLine {
43
+ itemPriceText: string;
44
+ itemTitle: string;
45
+ quantity: number;
46
+ skuAttrs?: SkuAttribute[];
47
+ }
48
+
49
+ // SKU attributes for product variants
50
+ export interface SkuAttribute {
51
+ name: string;
52
+ text: string;
53
+ }
54
+
55
+ // Action buttons available for each order
56
+ export interface OrderButton {
57
+ text: string;
58
+ type: string;
59
+ }
60
+
61
+ // Detailed tracking information
62
+ export interface TrackingInfo {
63
+ action: string;
64
+ address?: string;
65
+ date: string;
66
+ }
67
+
68
+ // Package tracking details
69
+ export interface PackageInfo {
70
+ expectDeliveryDate: string;
71
+ trackInfoList: TrackingInfo[];
72
+ trackingNumber: string;
73
+ }
74
+
75
+ // Logistic package block from order details
76
+ export interface LogisticPackageBlock extends ComponentData {
77
+ fields: {
78
+ packageInfoList: PackageInfo[];
79
+ };
80
+ tag: "detail_logistic_package_block";
81
+ type: "detail_logistic_package_block";
82
+ }
83
+
84
+ // Order status block from order details
85
+ export interface OrderStatusBlock extends ComponentData {
86
+ fields: {
87
+ title: string;
88
+ };
89
+ tag: "detail_order_status_block";
90
+ type: "detail_order_status_block";
91
+ }
92
+
93
+ // Order detail API response
94
+ export interface OrderDetailApiResponse extends AliExpressApiResponse {
95
+ api: "mtop.aliexpress.trade.buyer.order.detail";
96
+ data: {
97
+ data: Record<string, ComponentData>;
98
+ };
99
+ }
100
+
101
+ // Request data for order details API
102
+ export interface OrderDetailRequestData {
103
+ tradeOrderId: string;
104
+ timeZone: string;
105
+ clientPlatform: "pc";
106
+ channel: "tracking";
107
+ shipToCountry: string;
108
+ _lang: string;
109
+ }
110
+
111
+ export interface RequestConfig {
112
+ endpoint: string;
113
+ api: string;
114
+ data: any;
115
+ callbackName: string;
116
+ needLogin?: boolean;
117
+ method?: string;
118
+ }
119
+
120
+ export interface OrderListOptions {
121
+ statusTab?: string | null;
122
+ country?: string;
123
+ language?: string;
124
+ timezone?: string;
125
+ pageIndex?: number;
126
+ pageSize?: number;
127
+ }