cyymall-cli 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # cyymall-cli (`cyy`)
1
+ # cyymall-cli (`cyy`)
2
2
 
3
3
  Node.js CLI for CyyMall / 菜洋洋商城 APIs. Spec: [`../app-api-cli-spec.md`](../app-api-cli-spec.md).
4
4
 
@@ -25,7 +25,7 @@ Then run `cyy --help`.
25
25
 
26
26
  1. 在 [npmjs.com](https://www.npmjs.com/) 登录;发布策略若要求 **2FA**,必须先完成账号安全设置。
27
27
  2. 本机认证二选一(**不要把 token 写进仓库或发给他人**):
28
- - **`npm login`**(交互式);或
28
+ - **`npm login`**(交互式);或
29
29
  - **`npm config set //registry.npmjs.org/:_authToken=<token>`**(仅本机 `~/.npmrc`,勿提交 Git)。
30
30
  3. 在项目目录发布:
31
31
 
@@ -55,7 +55,8 @@ npm publish --otp=123456
55
55
 
56
56
  ```bash
57
57
  cyy config path
58
- cyy auth login --phone <mobile> --password <pwd>
58
+ cyy auth send-code --phone <mobile>
59
+ cyy auth login --phone <mobile> --code <sms>
59
60
  cyy auth whoami
60
61
  cyy shop list
61
62
  cyy shop sites --shop-id <门店ID>
@@ -69,23 +70,26 @@ cyy serve --port 8787
69
70
 
70
71
  ## Session vs bootstrap env
71
72
 
72
- **After `cyy auth login` succeeds**, the CLI reads the session token from **`data.token`** or **`data.appToken`** (plus optional **`shopId` / `siteId` / `versionCode` / `loginId`→`memberId`** when present) and saves them under **`~/.cyymall/config.json`**. If the API omits `shopId`/`siteId`, previously saved or bootstrap values are kept where applicable.
73
- 后续 **`api call`、`product search`、`order quick`** 等命令都会从该文件加载并组装 Header —— **正常情况下不再需要设置 `CYY_BOOTSTRAP_*`**,等价于「登录接口返回什么就持久化什么,进程退出后仍可用」(磁盘配置,不是仅限内存)。
73
+ **After `cyy auth login` succeeds**, the CLI reads the session token from **`data.token`** or **`data.appToken`** (plus optional **`shopId` / `siteId` / `versionCode` / `loginId`->`memberId`** when present) and saves them under **`~/.cyymall/config.json`**. If the API omits `shopId`/`siteId`, previously saved or bootstrap values are kept where applicable.
74
+ 后续 **`api call`、`product search`、`order quick`** 等命令都会从该文件加载并组装 Header,正常情况下不再需要设置 `CYY_BOOTSTRAP_*`。
74
75
 
75
76
  **`CYY_BOOTSTRAP_*` 仅用于「尚未登录」时**(例如第一次调用 `/app/auth/ua/registerMember/v2` 本身需要的网关 Header)。若你的环境里登录接口在未带 shop/token 时会被网关拒绝,再按需配置这些变量;**登录成功后的业务请求一律优先使用配置文件里的会话字段**。
76
77
 
77
78
  | Variable | Purpose |
78
79
  |----------|---------|
79
80
  | `CYY_BASE_URL` | Override API host (default `https://dhcmall.ifoodbuy.com`) |
80
- | `CYY_PASSWORD` | Used by `auth login` if `--password` omitted |
81
- | `CYY_BOOTSTRAP_TOKEN` | Optional **only before login**, gateway may expect placeholder token |
82
- | `CYY_BOOTSTRAP_SHOP_ID` | Optional **only before login** |
81
+ | `CYY_ENCRYPT_OFF` | `1` / `true`: disable hybrid crypto (plaintext `sendCodeV2`) |
82
+ | `CYY_ENCRYPT_DEBUG` | `1` / `true`: print RSA/AES diagnostics for hybrid decrypt failures |
83
+ | `CYY_BOOTSTRAP_TOKEN` | Optional - **only before login**, gateway may expect placeholder token |
84
+ | `CYY_BOOTSTRAP_SHOP_ID` | Optional - **only before login** |
83
85
  | `CYY_BOOTSTRAP_SITE_ID` | Default `1` when bootstrapping |
84
- | `CYY_BOOTSTRAP_MEMBER_ID` | Optional **only before login** |
86
+ | `CYY_BOOTSTRAP_MEMBER_ID` | Optional - **only before login** |
85
87
  | `CYY_BOOTSTRAP_VERSION_CODE` | App version header when bootstrapping / fallback (`22118`) |
86
88
 
89
+ `sendCodeV2` 固定使用 `src/embeddedCyyKeys.js` 中与 Android `cyy_native_key.cpp` 一致的内置密钥,不再支持从环境变量或文件覆盖公私钥。
90
+
87
91
  权威规格(含请求体附录)建议以 Android 工程内的 **`docs/app-api-cli-spec.md`** 为准;若与本仓库根目录同名文档不一致,以该版本为准。
88
92
 
89
93
  ## Implementation phases
90
94
 
91
- See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
95
+ See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
package/bin/cyy.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- require("../src/cli.js");
2
+ require("../src/cli.js");
package/package.json CHANGED
@@ -1,33 +1,33 @@
1
- {
2
- "name": "cyymall-cli",
3
- "version": "0.1.0",
4
- "description": "CyyMall / 菜洋洋商城 API CLI (per app-api-cli-spec)",
5
- "bin": {
6
- "cyy": "bin/cyy.js"
7
- },
8
- "main": "src/cli.js",
9
- "type": "commonjs",
10
- "files": [
11
- "bin",
12
- "src",
13
- "README.md",
14
- "package.json"
15
- ],
16
- "engines": {
17
- "node": ">=18"
18
- },
19
- "scripts": {
20
- "start": "node bin/cyy.js",
21
- "lint": "node --check src/cli.js",
22
- "prepublishOnly": "node --check src/cli.js && node --check bin/cyy.js"
23
- },
24
- "keywords": [
25
- "cyymall",
26
- "cli",
27
- "dhcmall"
28
- ],
29
- "license": "MIT",
30
- "dependencies": {
31
- "commander": "^12.1.0"
32
- }
33
- }
1
+ {
2
+ "name": "cyymall-cli",
3
+ "version": "0.1.2",
4
+ "description": "CyyMall / 菜洋洋商城 API CLI (per app-api-cli-spec)",
5
+ "bin": {
6
+ "cyy": "bin/cyy.js"
7
+ },
8
+ "main": "src/cli.js",
9
+ "type": "commonjs",
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "package.json"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "start": "node bin/cyy.js",
21
+ "lint": "node --check src/cli.js && node --check src/encrypt.js && node --check src/embeddedCyyKeys.js",
22
+ "prepublishOnly": "node --check src/cli.js && node --check src/encrypt.js && node --check src/embeddedCyyKeys.js && node --check bin/cyy.js"
23
+ },
24
+ "keywords": [
25
+ "cyymall",
26
+ "cli",
27
+ "dhcmall"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "commander": "^12.1.0"
32
+ }
33
+ }
package/src/biz.js CHANGED
@@ -1,14 +1,14 @@
1
- "use strict";
2
-
3
- /**
4
- * @param {unknown} upstream
5
- */
6
- function isBizSuccess(upstream) {
7
- if (upstream == null || typeof upstream !== "object") return false;
8
- const o = /** @type {{success?:boolean,code?:unknown}} */ (upstream);
9
- if (o.success === true) return true;
10
- if (o.code === "00000" || o.code === 0 || o.code === "0") return true;
11
- return false;
12
- }
13
-
14
- module.exports = { isBizSuccess };
1
+ "use strict";
2
+
3
+ /**
4
+ * @param {unknown} upstream
5
+ */
6
+ function isBizSuccess(upstream) {
7
+ if (upstream == null || typeof upstream !== "object") return false;
8
+ const o = /** @type {{success?:boolean,code?:unknown}} */ (upstream);
9
+ if (o.success === true) return true;
10
+ if (o.code === "00000" || o.code === 0 || o.code === "0") return true;
11
+ return false;
12
+ }
13
+
14
+ module.exports = { isBizSuccess };
package/src/cli.js CHANGED
@@ -1,208 +1,234 @@
1
- "use strict";
2
-
3
- const path = require("path");
4
- const pkg = require(path.join(__dirname, "..", "package.json"));
5
-
6
- const config = require("./config");
7
- const apiCall = require("./commands/apiCall");
8
- const auth = require("./commands/auth");
9
- const product = require("./commands/product");
10
- const cart = require("./commands/cart");
11
- const order = require("./commands/order");
12
- const serve = require("./commands/serve");
13
- const shop = require("./commands/shop");
14
-
15
- const { Command } = require("commander");
16
- const program = new Command();
17
-
18
- program
19
- .name("cyy")
20
- .description("CyyMall / 菜洋洋商城 CLI (see app-api-cli-spec.md)")
21
- .version(pkg.version);
22
-
23
- const cfgCmd = program.command("config").description("Configuration");
24
-
25
- cfgCmd
26
- .command("path")
27
- .description("Print config file path")
28
- .action(() => {
29
- console.log(config.getConfigPath());
30
- });
31
-
32
- cfgCmd
33
- .command("show")
34
- .description("Show config (token masked)")
35
- .action(() => {
36
- const c = config.loadConfig();
37
- if (!c) {
38
- console.log(JSON.stringify({ loggedIn: false }, null, 2));
39
- return;
40
- }
41
- const masked = { ...c, token: config.maskToken(String(c.token || "")) };
42
- console.log(JSON.stringify(masked, null, 2));
43
- });
44
-
45
- function collect(value, previous) {
46
- return previous.concat([value]);
47
- }
48
-
49
- const api = program.command("api").description("Low-level API calls");
50
-
51
- api
52
- .command("call")
53
- .description("Invoke any mapped endpoint (spec §3)")
54
- .requiredOption("--method <verb>", "HTTP method: GET, POST, PUT, PATCH, DELETE")
55
- .requiredOption("--module <name>", "DEFAULT | ORDER | PRODUCT | PLATFORM | PAYMENT | OSS | BIZ")
56
- .requiredOption("--path <path>", "Path starting with /, e.g. /app/product/getSkuList")
57
- .option("--body-json <json>", "JSON body string")
58
- .option("--body-file <file>", "Read JSON body from UTF-8 file")
59
- .option("--query <pair>", "Repeatable: key=value for query string", collect, [])
60
- .option("--header <h>", 'Repeatable: "Name: value"', collect, [])
61
- .option("--bare", "Bootstrap headers only (do not merge saved session)")
62
- .action(async (opts) => {
63
- await apiCall.runApiCall({
64
- ...opts,
65
- noAuth: Boolean(opts.bare),
66
- });
67
- });
68
-
69
- const authCmd = program.command("auth").description("Authentication");
70
-
71
- authCmd
72
- .command("login")
73
- .description("Password login (SHA-256); saves ~/.cyymall/config.json")
74
- .requiredOption("--phone <p>", "Mobile phone")
75
- .option("--password <pwd>", "Plain password (or set env CYY_PASSWORD)")
76
- .action(async (opts) => {
77
- const pwd = opts.password || process.env.CYY_PASSWORD;
78
- if (!pwd) {
79
- console.error("cyy: provide --password or set CYY_PASSWORD");
80
- process.exit(1);
81
- }
82
- await auth.login(opts.phone, pwd);
83
- });
84
-
85
- authCmd
86
- .command("whoami")
87
- .description("GET /member/getInfo/V2 using saved token")
88
- .action(async () => {
89
- await auth.whoami();
90
- });
91
-
92
- const prod = program.command("product").description("Product helpers");
93
-
94
- prod
95
- .command("search")
96
- .description("POST /mall-product/app/product/getSkuList")
97
- .requiredOption("--keyword <k>", "spuName keyword")
98
- .option("--shop-id <id>", "Override shopId (default from session)")
99
- .option("--site-id <id>", "Override siteId")
100
- .option("--page <n>", "pageNum", "1")
101
- .option("--page-size <n>", "pageSize", "10")
102
- .option("--stock-flag <s>", "stockFlag string (Java SearchCommodityReq)", "0")
103
- .action(async (opts) => {
104
- await product.search(opts);
105
- });
106
-
107
- const shopCmd = program.command("shop").description("Member shops and delivery sites");
108
-
109
- shopCmd
110
- .command("list")
111
- .description("POST /shop/member/list — paginated shops for current member (GetShopReq)")
112
- .option("--page <n>", "pageNum", "1")
113
- .option("--page-size <n>", "pageSize", "20")
114
- .option("--name <s>", "shopName filter", "")
115
- .option("--object-code <n>", "objectCode", "3")
116
- .action(async (opts) => {
117
- await shop.list(opts);
118
- });
119
-
120
- shopCmd
121
- .command("sites")
122
- .description("GET /shop/member/store/list — sites for a shop")
123
- .option("--shop-id <id>", "Shop id (default: session shop_id)")
124
- .option("--site-name <s>", "Optional siteName filter")
125
- .action(async (opts) => {
126
- await shop.sites(opts);
127
- });
128
-
129
- shopCmd
130
- .command("use")
131
- .description("Set default shop_id only in ~/.cyymall/config.json (site_id unchanged; validate via shop list)")
132
- .requiredOption("--shop-id <id>", "Numeric shop id from cyy shop list")
133
- .action(async (opts) => {
134
- await shop.useShop(opts);
135
- });
136
-
137
- shopCmd
138
- .command("use-site")
139
- .description("Set default site_id for current session shop_id (validates against shop sites)")
140
- .requiredOption("--site-id <id>", "Site id from shop sites")
141
- .action(async (opts) => {
142
- await shop.useSite(opts);
143
- });
144
-
145
- const cartCmd = program.command("cart").description("Shopping cart");
146
-
147
- cartCmd
148
- .command("add")
149
- .description("POST /mall-order/app/order/cart")
150
- .option("--body-file <file>", "Cart JSON body")
151
- .option("--body-json <json>", "Cart JSON string")
152
- .action(async (opts) => {
153
- await cart.add(opts);
154
- });
155
-
156
- const orderCmd = program.command("order").description("Order flow");
157
-
158
- orderCmd
159
- .command("pre-settle")
160
- .description("POST /mall-order/app/order/preSettleOrder")
161
- .option("--body-file <file>", "")
162
- .option("--body-json <json>", "")
163
- .action(async (opts) => {
164
- await order.preSettle(opts);
165
- });
166
-
167
- orderCmd
168
- .command("confirm")
169
- .description("POST /mall-order/app/order/confirmOrder")
170
- .option("--body-file <file>", "")
171
- .option("--body-json <json>", "")
172
- .action(async (opts) => {
173
- await order.confirm(opts);
174
- });
175
-
176
- orderCmd
177
- .command("pay-url")
178
- .description("Build H5 replacePay URL for order")
179
- .requiredOption("--order-id <id>", "Order id from confirm response")
180
- .action(async (opts) => {
181
- await order.payUrl(opts);
182
- });
183
-
184
- orderCmd
185
- .command("quick")
186
- .description("Search → cart → pre-settle → confirm → pay URL (first search hit)")
187
- .requiredOption("--keyword <k>", "Product keyword")
188
- .option("--quantity <n>", "Qty", "1")
189
- .option("--unit <u>", "袋 | 提 | 箱", "袋")
190
- .option("--shop-id <id>", "shopId override")
191
- .option("--site-id <id>", "siteId override")
192
- .action(async (opts) => {
193
- await order.quick(opts);
194
- });
195
-
196
- program
197
- .command("serve")
198
- .description("Local HTTP shim: POST /invoke JSON body { method, module, path, body?, query?, noAuth? }")
199
- .option("-p, --port <n>", "port", "8787")
200
- .option("--host <h>", "bind address", "127.0.0.1")
201
- .action((opts) => {
202
- serve.runServe({ port: opts.port, host: opts.host });
203
- });
204
-
205
- program.parseAsync(process.argv).catch((e) => {
206
- console.error(e);
207
- process.exit(1);
208
- });
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const pkg = require(path.join(__dirname, "..", "package.json"));
5
+
6
+ const config = require("./config");
7
+ const apiCall = require("./commands/apiCall");
8
+ const auth = require("./commands/auth");
9
+ const product = require("./commands/product");
10
+ const cart = require("./commands/cart");
11
+ const order = require("./commands/order");
12
+ const serve = require("./commands/serve");
13
+ const shop = require("./commands/shop");
14
+
15
+ const { Command } = require("commander");
16
+ const program = new Command();
17
+
18
+ program
19
+ .name("cyy")
20
+ .description("CyyMall / 菜洋洋商城 CLI (see app-api-cli-spec.md)")
21
+ .version(pkg.version);
22
+
23
+ const cfgCmd = program.command("config").description("Configuration");
24
+
25
+ cfgCmd
26
+ .command("path")
27
+ .description("Print config file path")
28
+ .action(() => {
29
+ console.log(config.getConfigPath());
30
+ });
31
+
32
+ cfgCmd
33
+ .command("show")
34
+ .description("Show config (token masked)")
35
+ .action(() => {
36
+ const c = config.loadConfig();
37
+ if (!c) {
38
+ console.log(JSON.stringify({ loggedIn: false }, null, 2));
39
+ return;
40
+ }
41
+ const masked = { ...c, token: config.maskToken(String(c.token || "")) };
42
+ console.log(JSON.stringify(masked, null, 2));
43
+ });
44
+
45
+ function collect(value, previous) {
46
+ return previous.concat([value]);
47
+ }
48
+
49
+ const api = program.command("api").description("Low-level API calls");
50
+
51
+ api
52
+ .command("call")
53
+ .description("Invoke any mapped endpoint (spec §3)")
54
+ .requiredOption("--method <verb>", "HTTP method: GET, POST, PUT, PATCH, DELETE")
55
+ .requiredOption("--module <name>", "DEFAULT | ORDER | PRODUCT | PLATFORM | PAYMENT | OSS | BIZ")
56
+ .requiredOption("--path <path>", "Path starting with /, e.g. /app/product/getSkuList")
57
+ .option("--body-json <json>", "JSON body string")
58
+ .option("--body-file <file>", "Read JSON body from UTF-8 file")
59
+ .option("--query <pair>", "Repeatable: key=value for query string", collect, [])
60
+ .option("--header <h>", 'Repeatable: "Name: value"', collect, [])
61
+ .option("--bare", "Bootstrap headers only (do not merge saved session)")
62
+ .action(async (opts) => {
63
+ await apiCall.runApiCall({
64
+ ...opts,
65
+ noAuth: Boolean(opts.bare),
66
+ });
67
+ });
68
+
69
+ const authCmd = program.command("auth").description("Authentication");
70
+
71
+ authCmd
72
+ .command("login")
73
+ .description("SMS login; sends code then waits for code input unless --code is provided")
74
+ .requiredOption("--phone <p>", "Mobile phone")
75
+ .option("--code <code>", "SMS verification code (skip interactive prompt)")
76
+ .action(async (opts) => {
77
+ await auth.login(opts.phone, opts.code);
78
+ });
79
+
80
+ authCmd
81
+ .command("send-code")
82
+ .description("Send SMS verification code for login")
83
+ .requiredOption("--phone <p>", "Mobile phone")
84
+ .action(async (opts) => {
85
+ await auth.sendCode(opts.phone);
86
+ });
87
+
88
+ authCmd
89
+ .command("whoami")
90
+ .description("GET /member/getInfo/V2 using saved token")
91
+ .action(async () => {
92
+ await auth.whoami();
93
+ });
94
+
95
+ const prod = program.command("product").description("Product helpers");
96
+
97
+ prod
98
+ .command("search")
99
+ .description("POST /mall-product/app/product/getSkuList")
100
+ .requiredOption("--keyword <k>", "spuName keyword")
101
+ .option("--shop-id <id>", "Override shopId (default from session)")
102
+ .option("--site-id <id>", "Override siteId")
103
+ .option("--page <n>", "pageNum", "1")
104
+ .option("--page-size <n>", "pageSize", "10")
105
+ .option("--stock-flag <s>", "stockFlag string (Java SearchCommodityReq)", "0")
106
+ .action(async (opts) => {
107
+ await product.search(opts);
108
+ });
109
+
110
+ const shopCmd = program.command("shop").description("Member shops and delivery sites");
111
+
112
+ shopCmd
113
+ .command("list")
114
+ .description("POST /shop/member/list paginated shops for current member (GetShopReq)")
115
+ .option("--page <n>", "pageNum", "1")
116
+ .option("--page-size <n>", "pageSize", "20")
117
+ .option("--name <s>", "shopName filter", "")
118
+ .option("--object-code <n>", "objectCode", "3")
119
+ .action(async (opts) => {
120
+ await shop.list(opts);
121
+ });
122
+
123
+ shopCmd
124
+ .command("sites")
125
+ .description("GET /shop/member/store/list sites for a shop")
126
+ .option("--shop-id <id>", "Shop id (default: session shop_id)")
127
+ .option("--site-name <s>", "Optional siteName filter")
128
+ .action(async (opts) => {
129
+ await shop.sites(opts);
130
+ });
131
+
132
+ shopCmd
133
+ .command("use")
134
+ .description("Set default shop_id only in ~/.cyymall/config.json (site_id unchanged; validate via shop list)")
135
+ .requiredOption("--shop-id <id>", "Numeric shop id from cyy shop list")
136
+ .action(async (opts) => {
137
+ await shop.useShop(opts);
138
+ });
139
+
140
+ shopCmd
141
+ .command("use-site")
142
+ .description("Set default site_id for current session shop_id (validates against shop sites)")
143
+ .requiredOption("--site-id <id>", "Site id from shop sites")
144
+ .action(async (opts) => {
145
+ await shop.useSite(opts);
146
+ });
147
+
148
+ const cartCmd = program.command("cart").description("Shopping cart");
149
+
150
+ cartCmd
151
+ .command("add")
152
+ .description("POST /mall-order/app/order/cart")
153
+ .option("--body-file <file>", "Cart JSON body")
154
+ .option("--body-json <json>", "Cart JSON string")
155
+ .action(async (opts) => {
156
+ await cart.add(opts);
157
+ });
158
+
159
+ const orderCmd = program.command("order").description("Order flow");
160
+
161
+ orderCmd
162
+ .command("pre-settle")
163
+ .description("POST /mall-order/app/order/preSettleOrder")
164
+ .option("--body-file <file>", "")
165
+ .option("--body-json <json>", "")
166
+ .action(async (opts) => {
167
+ await order.preSettle(opts);
168
+ });
169
+
170
+ orderCmd
171
+ .command("confirm")
172
+ .description("POST /mall-order/app/order/confirmOrder")
173
+ .option("--body-file <file>", "")
174
+ .option("--body-json <json>", "")
175
+ .action(async (opts) => {
176
+ await order.confirm(opts);
177
+ });
178
+
179
+ orderCmd
180
+ .command("pay-url")
181
+ .description("Build H5 replacePay URL for order")
182
+ .requiredOption("--order-id <id>", "Order id from confirm response")
183
+ .action(async (opts) => {
184
+ await order.payUrl(opts);
185
+ });
186
+
187
+ orderCmd
188
+ .command("list")
189
+ .description("POST /mall-multishop/order/queryPage (OrderQeq)")
190
+ .option("--page <n>", "pageNum", "1")
191
+ .option("--page-size <n>", "pageSize", "10")
192
+ .option("--status <s>", "status filter (empty = all)", "")
193
+ .option("--shop-id <id>", "shopId override")
194
+ .option("--object-code <n>", "objectCode", "1")
195
+ .option("--all", "set queryAllFlag true", false)
196
+ .option("--shop-keyword <s>", "shopKeyWord", "")
197
+ .action(async (opts) => {
198
+ await order.list(opts);
199
+ });
200
+
201
+ orderCmd
202
+ .command("cancel")
203
+ .description("POST /mall-order/app/order/cancelOrder")
204
+ .requiredOption("--order-id <id>", "Order id")
205
+ .option("--child-id <id>", "child order id (optional)")
206
+ .action(async (opts) => {
207
+ await order.cancel(opts);
208
+ });
209
+
210
+ orderCmd
211
+ .command("quick")
212
+ .description("Search → cart → pre-settle → confirm → pay URL (first search hit)")
213
+ .requiredOption("--keyword <k>", "Product keyword")
214
+ .option("--quantity <n>", "Qty", "1")
215
+ .option("--unit <u>", "袋 | 提 | 箱", "袋")
216
+ .option("--shop-id <id>", "shopId override")
217
+ .option("--site-id <id>", "siteId override")
218
+ .action(async (opts) => {
219
+ await order.quick(opts);
220
+ });
221
+
222
+ program
223
+ .command("serve")
224
+ .description("Local HTTP shim: POST /invoke JSON body { method, module, path, body?, query?, noAuth? }")
225
+ .option("-p, --port <n>", "port", "8787")
226
+ .option("--host <h>", "bind address", "127.0.0.1")
227
+ .action((opts) => {
228
+ serve.runServe({ port: opts.port, host: opts.host });
229
+ });
230
+
231
+ program.parseAsync(process.argv).catch((e) => {
232
+ console.error(e);
233
+ process.exit(1);
234
+ });