cyymall-cli 0.1.5 → 0.1.7

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
@@ -57,6 +57,7 @@ npm publish --otp=123456
57
57
  cyy config path
58
58
  cyy auth send-code --phone <mobile>
59
59
  cyy auth login --phone <mobile> --code <sms>
60
+ cyy auth import --token <appToken> [--member-id <id>] [--shop-id <id>] [--site-id <id>]
60
61
  cyy auth whoami
61
62
  cyy shop list
62
63
  cyy shop sites --shop-id <门店ID>
@@ -70,14 +71,28 @@ cyy serve --port 8787
70
71
 
71
72
  ## Session vs bootstrap env
72
73
 
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
+ **After `cyy auth login` or `cyy auth import` succeeds**, session fields live under **`~/.cyymall/config.json`** (`token`, `member_id`, `shop_id`, `site_id`, `version_code`, ). **`api call`、`product search`、`order quick`** 等命令读取**有效会话**:先合并配置文件,再用下表 `CYY_*` 覆盖(适合 Gateway / MCP 子进程注入、不落盘)。
75
75
 
76
- **`CYY_BOOTSTRAP_*` 仅用于「尚未登录」时**(例如第一次调用 `/app/auth/ua/registerMember/v2` 本身需要的网关 Header)。若你的环境里登录接口在未带 shop/token 时会被网关拒绝,再按需配置这些变量;**登录成功后的业务请求一律优先使用配置文件里的会话字段**。
76
+ **External token (skip SMS login):**
77
+
78
+ ```bash
79
+ cyy auth import --token "<appToken>" --member-id "<id>" --shop-id "<id>"
80
+ # or ephemeral for one process:
81
+ export CYY_TOKEN="<appToken>"
82
+ export CYY_SHOP_ID="<id>"
83
+ ```
84
+
85
+ **`CYY_BOOTSTRAP_*` 仅用于「尚未登录」时**(无 saved token 且无 `CYY_TOKEN`)。已登录时业务请求优先使用配置文件 + `CYY_TOKEN` 等会话变量。
77
86
 
78
87
  | Variable | Purpose |
79
88
  |----------|---------|
80
89
  | `CYY_BASE_URL` | Override API host (default `https://dhcmall.ifoodbuy.com`) |
90
+ | `CYY_TOKEN` | Session token for current process (overrides saved config) |
91
+ | `CYY_MEMBER_ID` | Override `member_id` header |
92
+ | `CYY_SHOP_ID` | Override `shop_id` header |
93
+ | `CYY_SITE_ID` | Override `site_id` header |
94
+ | `CYY_VERSION_CODE` | Override `version_code` header |
95
+ | `CYY_PHONE` | Optional label in config display |
81
96
  | `CYY_ENCRYPT_OFF` | `1` / `true`: disable hybrid crypto (plaintext `sendCodeV2`) |
82
97
  | `CYY_ENCRYPT_DEBUG` | `1` / `true`: print RSA/AES diagnostics for hybrid decrypt failures |
83
98
  | `CYY_BOOTSTRAP_TOKEN` | Optional - **only before login**, gateway may expect placeholder token |
@@ -92,4 +107,4 @@ cyy serve --port 8787
92
107
 
93
108
  ## Implementation phases
94
109
 
95
- See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
110
+ See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyymall-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CyyMall / 菜洋洋商城 API CLI (per app-api-cli-spec)",
5
5
  "bin": {
6
6
  "cyy": "bin/cyy.js"
package/src/Untitled ADDED
@@ -0,0 +1 @@
1
+ shop
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ const path = require("path");
4
4
  const pkg = require(path.join(__dirname, "..", "package.json"));
5
5
 
6
6
  const config = require("./config");
7
+ const http = require("./http");
7
8
  const apiCall = require("./commands/apiCall");
8
9
  const auth = require("./commands/auth");
9
10
  const product = require("./commands/product");
@@ -31,14 +32,26 @@ cfgCmd
31
32
 
32
33
  cfgCmd
33
34
  .command("show")
34
- .description("Show config (token masked)")
35
+ .description("Show config (token masked); includes effective session and env overrides")
35
36
  .action(() => {
36
- const c = config.loadConfig();
37
- if (!c) {
37
+ const file = config.loadConfig();
38
+ const session = config.getSession();
39
+ const envOverrides = config.getEnvSessionOverrideKeys();
40
+ if (!config.hasAuthToken(session) && !file) {
38
41
  console.log(JSON.stringify({ loggedIn: false }, null, 2));
39
42
  return;
40
43
  }
41
- const masked = { ...c, token: config.maskToken(String(c.token || "")) };
44
+ const masked = {
45
+ loggedIn: config.hasAuthToken(session),
46
+ saved: file
47
+ ? { ...file, token: config.maskToken(String(file.token || "")) }
48
+ : null,
49
+ effective: {
50
+ ...session,
51
+ token: config.maskToken(String(session.token || "")),
52
+ },
53
+ envOverrides: envOverrides.length ? envOverrides : undefined,
54
+ };
42
55
  console.log(JSON.stringify(masked, null, 2));
43
56
  });
44
57
 
@@ -92,6 +105,32 @@ authCmd
92
105
  await auth.whoami();
93
106
  });
94
107
 
108
+ authCmd
109
+ .command("import")
110
+ .description(
111
+ "Import external session token into ~/.cyymall/config.json (skip SMS login); optional --no-verify",
112
+ )
113
+ .option("--token <t>", "App session token (appToken / Authorization value)")
114
+ .option("--token-file <file>", "Read token from UTF-8 file (e.g. secret mount)")
115
+ .option("--member-id <id>", "memberId header")
116
+ .option("--shop-id <id>", "shopId header")
117
+ .option("--site-id <id>", "siteId header (default 1 if omitted and not in saved config)")
118
+ .option("--version-code <n>", "version-code header")
119
+ .option("--phone <p>", "Optional phone label for config display")
120
+ .option("--no-verify", "Save without calling GET /member/getInfo/V2")
121
+ .action(async (opts) => {
122
+ await auth.importSession({
123
+ token: opts.token,
124
+ tokenFile: opts.tokenFile,
125
+ memberId: opts.memberId,
126
+ shopId: opts.shopId,
127
+ siteId: opts.siteId,
128
+ versionCode: opts.versionCode,
129
+ phone: opts.phone,
130
+ noVerify: Boolean(opts.noVerify),
131
+ });
132
+ });
133
+
95
134
  const prod = program.command("product").description("Product helpers");
96
135
 
97
136
  prod
@@ -74,7 +74,7 @@ async function executeApiCall(opts) {
74
74
  let headers = {};
75
75
 
76
76
  if (!noAuth) {
77
- const cfg = config.loadConfig();
77
+ const cfg = config.getSession();
78
78
  headers = /** @type {Record<string,string>} */ (
79
79
  http.buildAuthHeaders(cfg)
80
80
  );
@@ -1,5 +1,6 @@
1
- "use strict";
1
+ "use strict";
2
2
 
3
+ const fs = require("fs");
3
4
  const crypto = require("crypto");
4
5
  const readline = require("readline/promises");
5
6
  const { stdin: input, stdout: output } = require("process");
@@ -9,15 +10,15 @@ const biz = require("../biz");
9
10
  const encrypt = require("../encrypt");
10
11
 
11
12
  /**
12
- * `/app/auth/ua/*` responses (e.g. sendCodeV2, registerMember/v2): raw wire -> decryptResponse ->
13
- * JSON.parse -> optional parsedJsonAfterHybridEnvelopeDecrypt, same order as App.
13
+ * `/app/auth/ua/sendCodeV2` responses may still be returned as a hybrid envelope.
14
+ * Decrypt the raw wire body first, then parse JSON, matching the App pipeline.
14
15
  *
15
16
  * @param {string} [rawWireText]
16
17
  * @param {unknown} jsonAfterParse
17
- * @param {string} clientPkForDecrypt
18
18
  * @returns {unknown}
19
19
  */
20
- function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse, clientPkForDecrypt) {
20
+ function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse) {
21
+ const { clientPrivateKey } = encrypt.resolveEncryptKeys();
21
22
  let wire =
22
23
  typeof rawWireText === "string" && rawWireText.trim()
23
24
  ? rawWireText.trim()
@@ -44,26 +45,26 @@ function plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse, clientPkForDe
44
45
  }
45
46
  }
46
47
 
47
- if (wire) {
48
- const decryptedText = encrypt.decryptResponse(wire, clientPkForDecrypt);
48
+ if (wire && clientPrivateKey) {
49
+ const decryptedText = encrypt.decryptResponse(wire, clientPrivateKey);
49
50
  try {
50
51
  const obj = decryptedText ? JSON.parse(decryptedText) : null;
51
52
  if (obj != null && typeof obj === "object" && !Array.isArray(obj)) {
52
53
  const out = encrypt.looksLikeHybridEnvelope(obj)
53
- ? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(obj, clientPkForDecrypt)
54
+ ? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(obj, clientPrivateKey)
54
55
  : obj;
55
56
  if (!encrypt.looksLikeHybridEnvelope(out)) {
56
57
  return out;
57
58
  }
58
59
  }
59
60
  } catch {
60
- // fall through to parsed-json fallback
61
+ // fall through to parsed fallback
61
62
  }
62
63
  }
63
64
 
64
65
  if (jsonAfterParse != null && typeof jsonAfterParse === "object" && !Array.isArray(jsonAfterParse)) {
65
- return encrypt.looksLikeHybridEnvelope(jsonAfterParse)
66
- ? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(jsonAfterParse, clientPkForDecrypt)
66
+ return clientPrivateKey && encrypt.looksLikeHybridEnvelope(jsonAfterParse)
67
+ ? encrypt.parsedJsonAfterHybridEnvelopeDecrypt(jsonAfterParse, clientPrivateKey)
67
68
  : jsonAfterParse;
68
69
  }
69
70
 
@@ -91,31 +92,24 @@ function extractBizMessage(json, fallback) {
91
92
  : fallback;
92
93
  }
93
94
 
94
- function buildHybridAuthBody(plainBody, headers) {
95
+ function buildEncryptedSendCodeBody(phone) {
96
+ const headers = /** @type {Record<string,string>} */ (http.buildDefaultHeaders());
97
+ const plainBody = JSON.stringify({ phone, objectCode: "3" });
95
98
  if (!encrypt.encryptEnvConfigured()) {
96
- return plainBody;
99
+ return { headers, body: plainBody };
97
100
  }
98
101
  headers.encrypte = "true";
99
102
  const { serverPublicKey } = encrypt.resolveEncryptKeys();
100
103
  const hy = encrypt.hybridEncrypt(plainBody, serverPublicKey);
101
- return encrypt.hybridEncryptResultToJson(hy);
102
- }
103
-
104
- function decryptAuthBizJson(rawWireText, jsonAfterParse) {
105
- const { clientPrivateKey } = encrypt.resolveEncryptKeys();
106
- if (!clientPrivateKey) return jsonAfterParse;
107
- return plainBizJsonFromSendCodeWire(
108
- typeof rawWireText === "string" ? rawWireText : "",
109
- jsonAfterParse,
110
- clientPrivateKey,
111
- );
104
+ return {
105
+ headers,
106
+ body: encrypt.hybridEncryptResultToJson(hy),
107
+ };
112
108
  }
113
109
 
114
110
  async function requestSmsCode(phone) {
115
111
  const url = http.moduleUrl("DEFAULT", "/app/auth/ua/sendCodeV2");
116
- const headers = /** @type {Record<string,string>} */ (http.buildDefaultHeaders());
117
- const plainBody = JSON.stringify({ phone, objectCode: "3" });
118
- const body = buildHybridAuthBody(plainBody, headers);
112
+ const { headers, body } = buildEncryptedSendCodeBody(phone);
119
113
 
120
114
  const { ok, status, json: jsonAfterParse, rawWireText } = await http.request(url, {
121
115
  method: "POST",
@@ -125,7 +119,7 @@ async function requestSmsCode(phone) {
125
119
  includeRawWireText: true,
126
120
  });
127
121
 
128
- const json = decryptAuthBizJson(rawWireText, jsonAfterParse);
122
+ const json = plainBizJsonFromSendCodeWire(rawWireText, jsonAfterParse);
129
123
 
130
124
  if (!ok) {
131
125
  const msg = extractBizMessage(json, `send login code failed (HTTP ${status})`);
@@ -134,26 +128,24 @@ async function requestSmsCode(phone) {
134
128
  process.exit(2);
135
129
  }
136
130
 
137
- // User-requested behavior: once the send-code request itself succeeds over HTTP,
138
- // treat SMS dispatch as successful even if the encrypted response body cannot be
139
- // decrypted into upstream business JSON.
140
- return { status, json, acceptedByGateway: true };
131
+ // Only sendCodeV2 is encrypted. Once transport succeeds, treat the SMS request as accepted.
132
+ return { status, json, gatewayAccepted: true };
141
133
  }
142
134
 
143
135
  async function sendCode(phone) {
144
- const { status, json, acceptedByGateway } = await requestSmsCode(phone);
136
+ const { status, json, gatewayAccepted } = await requestSmsCode(phone);
145
137
  const traceId = buildTraceId();
146
138
  console.log(
147
139
  JSON.stringify(
148
140
  {
149
141
  success: true,
150
142
  code: "OK",
151
- message: acceptedByGateway ? "code sent" : "code send accepted",
143
+ message: "code sent",
152
144
  data: {
153
145
  phone,
154
146
  objectCode: "3",
155
147
  httpStatus: status,
156
- gatewayAccepted: acceptedByGateway,
148
+ gatewayAccepted,
157
149
  upstream: json,
158
150
  },
159
151
  traceId,
@@ -168,7 +160,7 @@ async function sendCode(phone) {
168
160
  async function loginWithCode(phone, code) {
169
161
  const url = http.moduleUrl("DEFAULT", "/app/auth/ua/registerMember/v2");
170
162
  const headers = /** @type {Record<string,string>} */ (http.buildDefaultHeaders());
171
- const plainBody = JSON.stringify({
163
+ const body = JSON.stringify({
172
164
  phone,
173
165
  objectCode: "3",
174
166
  password: "",
@@ -177,18 +169,13 @@ async function loginWithCode(phone, code) {
177
169
  smsCode: code,
178
170
  wxOpenid: "",
179
171
  });
180
- const body = buildHybridAuthBody(plainBody, headers);
181
172
 
182
- const { ok, json: jsonAfterParse, rawWireText } = await http.request(url, {
173
+ const { ok, json } = await http.request(url, {
183
174
  method: "POST",
184
175
  headers,
185
176
  body,
186
- decryptHybridResponse: false,
187
- includeRawWireText: true,
188
177
  });
189
178
 
190
- const json = decryptAuthBizJson(rawWireText, jsonAfterParse);
191
-
192
179
  if (!ok || !biz.isBizSuccess(json)) {
193
180
  const msg = extractBizMessage(json, "login failed");
194
181
  console.error(`cyy: ${msg}`);
@@ -202,8 +189,8 @@ async function loginWithCode(phone, code) {
202
189
 
203
190
  const loginId = String(data.loginId || "");
204
191
  const memberId = loginId.includes("_") ? loginId.split("_")[0] : loginId;
205
-
206
192
  const token = pickLoginToken(data);
193
+
207
194
  if (!token) {
208
195
  console.error(
209
196
  "cyy: login reported success but data has no token (expected token, appToken, or accessToken)",
@@ -272,12 +259,89 @@ async function login(phone, code) {
272
259
  await loginWithCode(phone, smsCode);
273
260
  }
274
261
 
275
- async function whoami() {
276
- const cfg = config.loadConfig();
277
- if (!cfg?.token) {
278
- console.error("cyy: not logged in. Run: cyy auth login --phone ...");
262
+ /**
263
+ * Persist an externally supplied session (Native App / Gateway token), skipping SMS login.
264
+ * @param {object} opts
265
+ * @param {string} [opts.token]
266
+ * @param {string} [opts.tokenFile]
267
+ * @param {string} [opts.memberId]
268
+ * @param {string} [opts.shopId]
269
+ * @param {string} [opts.siteId]
270
+ * @param {string} [opts.versionCode]
271
+ * @param {string} [opts.phone]
272
+ * @param {boolean} [opts.noVerify]
273
+ */
274
+ async function importSession(opts) {
275
+ let token = opts.token != null ? String(opts.token).trim() : "";
276
+ if (!token && opts.tokenFile) {
277
+ token = fs.readFileSync(opts.tokenFile, "utf8").trim();
278
+ }
279
+ if (!token) {
280
+ console.error("cyy: --token or --token-file is required");
279
281
  process.exit(1);
280
282
  }
283
+
284
+ const prev = config.loadConfig() || {};
285
+ const saved = {
286
+ ...prev,
287
+ token,
288
+ member_id:
289
+ opts.memberId != null && String(opts.memberId).trim() !== ""
290
+ ? String(opts.memberId).trim()
291
+ : String(prev.member_id || ""),
292
+ shop_id:
293
+ opts.shopId != null && String(opts.shopId).trim() !== ""
294
+ ? String(opts.shopId).trim()
295
+ : String(prev.shop_id || ""),
296
+ site_id:
297
+ opts.siteId != null && String(opts.siteId).trim() !== ""
298
+ ? String(opts.siteId).trim()
299
+ : String(prev.site_id || "1"),
300
+ version_code: String(
301
+ opts.versionCode != null && String(opts.versionCode).trim() !== ""
302
+ ? opts.versionCode
303
+ : prev.version_code ?? http.DEFAULT_VERSION,
304
+ ),
305
+ phone:
306
+ opts.phone != null && String(opts.phone).trim() !== ""
307
+ ? String(opts.phone).trim()
308
+ : String(prev.phone || ""),
309
+ login_time: Math.floor(Date.now() / 1000),
310
+ session_source: "import",
311
+ };
312
+
313
+ config.saveConfig(saved);
314
+
315
+ if (opts.noVerify) {
316
+ const traceId = buildTraceId();
317
+ console.log(
318
+ JSON.stringify(
319
+ {
320
+ success: true,
321
+ code: "OK",
322
+ message: "session imported (not verified)",
323
+ data: {
324
+ phone: saved.phone,
325
+ member_id: saved.member_id,
326
+ shop_id: saved.shop_id,
327
+ site_id: saved.site_id,
328
+ version_code: saved.version_code,
329
+ token_preview: config.maskToken(saved.token),
330
+ },
331
+ traceId,
332
+ },
333
+ null,
334
+ 2,
335
+ ),
336
+ );
337
+ process.exit(0);
338
+ }
339
+
340
+ await whoami();
341
+ }
342
+
343
+ async function whoami() {
344
+ const cfg = config.requireAuthSession();
281
345
  const url = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
282
346
  const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
283
347
  delete headers["Content-Type"];
@@ -305,4 +369,4 @@ async function whoami() {
305
369
  process.exit(success ? 0 : 2);
306
370
  }
307
371
 
308
- module.exports = { login, loginWithCode, sendCode, whoami };
372
+ module.exports = { login, loginWithCode, sendCode, whoami, importSession };
@@ -10,11 +10,7 @@ const biz = require("../biz");
10
10
  * @param {{ bodyFile?: string, bodyJson?: string }} opts
11
11
  */
12
12
  async function add(opts) {
13
- const cfg = config.loadConfig();
14
- if (!cfg?.token) {
15
- console.error("cyy: not logged in.");
16
- process.exit(1);
17
- }
13
+ const cfg = config.requireAuthSession();
18
14
 
19
15
  let raw = opts.bodyJson;
20
16
  if (opts.bodyFile) {
@@ -28,11 +28,7 @@ function envelope(success, message, data, exitCode) {
28
28
  * @param {{ bodyFile?: string, bodyJson?: string }} opts
29
29
  */
30
30
  async function preSettle(opts) {
31
- const cfg = config.loadConfig();
32
- if (!cfg?.token) {
33
- console.error("cyy: not logged in.");
34
- process.exit(1);
35
- }
31
+ const cfg = config.requireAuthSession();
36
32
  let raw = opts.bodyJson;
37
33
  if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
38
34
  if (!raw) {
@@ -54,11 +50,7 @@ async function preSettle(opts) {
54
50
  * @param {{ bodyFile?: string, bodyJson?: string }} opts
55
51
  */
56
52
  async function confirm(opts) {
57
- const cfg = config.loadConfig();
58
- if (!cfg?.token) {
59
- console.error("cyy: not logged in.");
60
- process.exit(1);
61
- }
53
+ const cfg = config.requireAuthSession();
62
54
  let raw = opts.bodyJson;
63
55
  if (opts.bodyFile) raw = fs.readFileSync(opts.bodyFile, "utf8");
64
56
  if (!raw) {
@@ -77,11 +69,7 @@ async function confirm(opts) {
77
69
  }
78
70
 
79
71
  async function payUrl(opts) {
80
- const cfg = config.loadConfig();
81
- if (!cfg?.token) {
82
- console.error("cyy: not logged in.");
83
- process.exit(1);
84
- }
72
+ const cfg = config.requireAuthSession();
85
73
  const orderId = opts.orderId;
86
74
  if (!orderId) {
87
75
  console.error("cyy: --order-id required");
@@ -118,11 +106,7 @@ async function payUrl(opts) {
118
106
  * @param {object} opts
119
107
  */
120
108
  async function quick(opts) {
121
- const cfg = config.loadConfig();
122
- if (!cfg?.token) {
123
- console.error("cyy: not logged in.");
124
- process.exit(1);
125
- }
109
+ const cfg = config.requireAuthSession();
126
110
 
127
111
  const keyword = opts.keyword;
128
112
  const quantity = Number(opts.quantity || 1);
@@ -329,11 +313,7 @@ async function quick(opts) {
329
313
  * @param {{ page?: string, pageSize?: string, status?: string, shopId?: string, objectCode?: string, all?: boolean, shopKeyword?: string }} opts
330
314
  */
331
315
  async function list(opts) {
332
- const cfg = config.loadConfig();
333
- if (!cfg?.token) {
334
- console.error("cyy: not logged in.");
335
- process.exit(1);
336
- }
316
+ const cfg = config.requireAuthSession();
337
317
  const shopId = opts.shopId != null && opts.shopId !== "" ? Number(opts.shopId) : Number(cfg.shop_id);
338
318
  if (!Number.isFinite(shopId)) {
339
319
  console.error("cyy: shopId missing (set shop_id in config or pass --shop-id)");
@@ -371,11 +351,7 @@ async function list(opts) {
371
351
  * @param {{ orderId?: string, childId?: string }} opts
372
352
  */
373
353
  async function cancel(opts) {
374
- const cfg = config.loadConfig();
375
- if (!cfg?.token) {
376
- console.error("cyy: not logged in.");
377
- process.exit(1);
378
- }
354
+ const cfg = config.requireAuthSession();
379
355
  const orderId = opts.orderId;
380
356
  if (!orderId) {
381
357
  console.error("cyy: --order-id required");
@@ -9,11 +9,7 @@ const biz = require("../biz");
9
9
  * @param {object} opts
10
10
  */
11
11
  async function search(opts) {
12
- const cfg = config.loadConfig();
13
- if (!cfg?.token) {
14
- console.error("cyy: not logged in. Run: cyy auth login");
15
- process.exit(1);
16
- }
12
+ const cfg = config.requireAuthSession();
17
13
 
18
14
  const shopId = Number(opts.shopId ?? cfg.shop_id);
19
15
  const siteId = Number(opts.siteId ?? cfg.site_id);
@@ -28,11 +28,7 @@ function envelope(success, message, upstream, exitCode) {
28
28
  * @param {{ page?: string, pageSize?: string, name?: string, objectCode?: string }} opts
29
29
  */
30
30
  async function list(opts) {
31
- const cfg = config.loadConfig();
32
- if (!cfg?.token) {
33
- console.error("cyy: not logged in. Run: cyy auth login");
34
- process.exit(1);
35
- }
31
+ const cfg = config.requireAuthSession();
36
32
 
37
33
  const body = JSON.stringify({
38
34
  pageNum: Number(opts.page || 1),
@@ -62,11 +58,7 @@ async function list(opts) {
62
58
  * @param {{ shopId?: string, siteName?: string }} opts
63
59
  */
64
60
  async function sites(opts) {
65
- const cfg = config.loadConfig();
66
- if (!cfg?.token) {
67
- console.error("cyy: not logged in. Run: cyy auth login");
68
- process.exit(1);
69
- }
61
+ const cfg = config.requireAuthSession();
70
62
 
71
63
  const shopId = opts.shopId ?? cfg.shop_id;
72
64
  if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
@@ -179,11 +171,7 @@ async function shopIdBelongsToMember(cfg, shopId) {
179
171
  * @param {{ shopId?: string }} opts
180
172
  */
181
173
  async function useShop(opts) {
182
- const cfg = config.loadConfig();
183
- if (!cfg?.token) {
184
- console.error("cyy: not logged in. Run: cyy auth login");
185
- process.exit(1);
186
- }
174
+ const cfg = config.requireAuthSession();
187
175
  const shopId = opts.shopId;
188
176
  if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
189
177
  console.error("cyy: shop use requires --shop-id");
@@ -229,11 +217,7 @@ async function useShop(opts) {
229
217
  * @param {{ siteId?: string }} opts
230
218
  */
231
219
  async function useSite(opts) {
232
- const cfg = config.loadConfig();
233
- if (!cfg?.token) {
234
- console.error("cyy: not logged in. Run: cyy auth login");
235
- process.exit(1);
236
- }
220
+ const cfg = config.requireAuthSession();
237
221
  const shopId = cfg.shop_id;
238
222
  if (shopId === undefined || shopId === null || String(shopId).trim() === "") {
239
223
  console.error("cyy: no shop_id in session; run: cyy shop use --shop-id <id>");
package/src/config.js CHANGED
@@ -4,7 +4,7 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const os = require("os");
6
6
 
7
- const DEFAULT_BASE_URL = "https://dhcmall.ifoodbuy.com";
7
+ const DEFAULT_BASE_URL = "https://yunping.ifoodbuy.com/";
8
8
 
9
9
  /** BuildConfig-style module prefixes (app-api-cli-spec §1.2) */
10
10
  const MODULE_MAP = {
@@ -69,9 +69,71 @@ function maskToken(t) {
69
69
  return `${t.slice(0, 8)}...${t.slice(-4)}`;
70
70
  }
71
71
 
72
+ /** Env keys that override ~/.cyymall/config.json for the current process (ephemeral). */
73
+ const SESSION_ENV_KEYS = [
74
+ ["token", "CYY_TOKEN"],
75
+ ["member_id", "CYY_MEMBER_ID"],
76
+ ["shop_id", "CYY_SHOP_ID"],
77
+ ["site_id", "CYY_SITE_ID"],
78
+ ["version_code", "CYY_VERSION_CODE"],
79
+ ["phone", "CYY_PHONE"],
80
+ ];
81
+
82
+ /**
83
+ * Effective session: saved config merged with CYY_* env overrides (env wins when set).
84
+ * @returns {Record<string, unknown>}
85
+ */
86
+ function getSession() {
87
+ const file = loadConfig() || {};
88
+ /** @type {Record<string, unknown>} */
89
+ const merged = { ...file };
90
+ for (const [field, envKey] of SESSION_ENV_KEYS) {
91
+ const v = process.env[envKey];
92
+ if (v != null && String(v).trim() !== "") {
93
+ merged[field] = String(v).trim();
94
+ }
95
+ }
96
+ return merged;
97
+ }
98
+
99
+ /**
100
+ * @param {Record<string, unknown>|null|undefined} [session]
101
+ */
102
+ function hasAuthToken(session) {
103
+ const s = session ?? getSession();
104
+ const t = s?.token;
105
+ return t != null && String(t).trim() !== "";
106
+ }
107
+
108
+ /**
109
+ * @returns {Record<string, unknown>}
110
+ */
111
+ function requireAuthSession() {
112
+ if (!hasAuthToken()) {
113
+ console.error(
114
+ "cyy: not logged in. Use one of: cyy auth login --phone ... | cyy auth import --token ... | CYY_TOKEN=...",
115
+ );
116
+ process.exit(1);
117
+ }
118
+ return getSession();
119
+ }
120
+
121
+ /** Names of CYY_* env vars currently overriding saved config. */
122
+ function getEnvSessionOverrideKeys() {
123
+ const active = [];
124
+ for (const [, envKey] of SESSION_ENV_KEYS) {
125
+ const v = process.env[envKey];
126
+ if (v != null && String(v).trim() !== "") {
127
+ active.push(envKey);
128
+ }
129
+ }
130
+ return active;
131
+ }
132
+
72
133
  module.exports = {
73
134
  DEFAULT_BASE_URL,
74
135
  MODULE_MAP,
136
+ SESSION_ENV_KEYS,
75
137
  getBaseUrl,
76
138
  getConfigDir,
77
139
  getConfigPath,
@@ -79,4 +141,8 @@ module.exports = {
79
141
  loadConfig,
80
142
  saveConfig,
81
143
  maskToken,
144
+ getSession,
145
+ hasAuthToken,
146
+ requireAuthSession,
147
+ getEnvSessionOverrideKeys,
82
148
  };
package/src/http.js CHANGED
@@ -46,8 +46,7 @@ function buildAuthHeaders(cfg) {
46
46
 
47
47
  /** Headers for unauthenticated calls (e.g. login) — still need default shop/site for gateway */
48
48
  function buildDefaultHeaders() {
49
- const cfg = config.loadConfig();
50
- return buildAuthHeaders(cfg);
49
+ return buildAuthHeaders(config.getSession());
51
50
  }
52
51
 
53
52
  /**