cyymall-cli 0.1.6 → 0.1.8

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.6",
3
+ "version": "0.1.8",
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
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");
@@ -258,12 +259,89 @@ async function login(phone, code) {
258
259
  await loginWithCode(phone, smsCode);
259
260
  }
260
261
 
261
- async function whoami() {
262
- const cfg = config.loadConfig();
263
- if (!cfg?.token) {
264
- 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");
265
281
  process.exit(1);
266
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();
267
345
  const url = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
268
346
  const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
269
347
  delete headers["Content-Type"];
@@ -291,4 +369,4 @@ async function whoami() {
291
369
  process.exit(success ? 0 : 2);
292
370
  }
293
371
 
294
- 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
  /**