@leeguoo/yapi-mcp 0.3.22 → 0.3.24
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 +33 -5
- package/dist/docs/markdown.d.ts +9 -0
- package/dist/docs/markdown.js +16 -0
- package/dist/docs/markdown.js.map +1 -1
- package/dist/services/yapi/authCache.d.ts +1 -0
- package/dist/services/yapi/authCache.js +7 -0
- package/dist/services/yapi/authCache.js.map +1 -1
- package/dist/yapi-cli.js +520 -55
- package/dist/yapi-cli.js.map +1 -1
- package/package.json +2 -1
- package/skill-template/SKILL.md +17 -4
package/dist/yapi-cli.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const os_1 = __importDefault(require("os"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
@@ -12,12 +13,38 @@ const readline_1 = __importDefault(require("readline"));
|
|
|
12
13
|
const markdown_1 = require("./docs/markdown");
|
|
13
14
|
const install_1 = require("./skill/install");
|
|
14
15
|
const auth_1 = require("./services/yapi/auth");
|
|
16
|
+
const authCache_1 = require("./services/yapi/authCache");
|
|
17
|
+
class HttpStatusError extends Error {
|
|
18
|
+
status;
|
|
19
|
+
statusText;
|
|
20
|
+
body;
|
|
21
|
+
endpoint;
|
|
22
|
+
constructor(endpoint, status, statusText, body) {
|
|
23
|
+
super(`request failed: ${status} ${statusText} ${body}`);
|
|
24
|
+
this.name = "HttpStatusError";
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.statusText = statusText;
|
|
27
|
+
this.body = body;
|
|
28
|
+
this.endpoint = endpoint;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
15
31
|
function parseKeyValue(raw) {
|
|
16
32
|
if (!raw || !raw.includes("="))
|
|
17
33
|
throw new Error("expected key=value");
|
|
18
34
|
const idx = raw.indexOf("=");
|
|
19
35
|
return [raw.slice(0, idx), raw.slice(idx + 1)];
|
|
20
36
|
}
|
|
37
|
+
function parseQueryArg(raw) {
|
|
38
|
+
const trimmed = String(raw || "").trim().replace(/^\?/, "");
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
throw new Error("expected key=value");
|
|
41
|
+
if (!trimmed.includes("&"))
|
|
42
|
+
return [parseKeyValue(trimmed)];
|
|
43
|
+
const items = Array.from(new URLSearchParams(trimmed).entries()).filter(([key]) => Boolean(key));
|
|
44
|
+
if (items.length)
|
|
45
|
+
return items;
|
|
46
|
+
return [parseKeyValue(trimmed)];
|
|
47
|
+
}
|
|
21
48
|
function parseHeader(raw) {
|
|
22
49
|
if (!raw || !raw.includes(":"))
|
|
23
50
|
throw new Error("expected Header:Value");
|
|
@@ -32,6 +59,58 @@ function parseJsonMaybe(text) {
|
|
|
32
59
|
return null;
|
|
33
60
|
}
|
|
34
61
|
}
|
|
62
|
+
function formatBytes(bytes) {
|
|
63
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
64
|
+
return "0 B";
|
|
65
|
+
const units = ["B", "KiB", "MiB", "GiB"];
|
|
66
|
+
let value = bytes;
|
|
67
|
+
let index = 0;
|
|
68
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
69
|
+
value /= 1024;
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
return `${value >= 10 || index === 0 ? value.toFixed(0) : value.toFixed(2)} ${units[index]}`;
|
|
73
|
+
}
|
|
74
|
+
function parseByteSize(raw) {
|
|
75
|
+
const text = String(raw || "");
|
|
76
|
+
const bytesMatch = text.match(/(\d+)\s*bytes?/i);
|
|
77
|
+
if (bytesMatch)
|
|
78
|
+
return Number(bytesMatch[1]);
|
|
79
|
+
const unitMatch = text.match(/(\d+(?:\.\d+)?)\s*(kib|kb|mib|mb|gib|gb)\b/i);
|
|
80
|
+
if (!unitMatch)
|
|
81
|
+
return null;
|
|
82
|
+
const value = Number(unitMatch[1]);
|
|
83
|
+
if (!Number.isFinite(value))
|
|
84
|
+
return null;
|
|
85
|
+
const unit = unitMatch[2].toLowerCase();
|
|
86
|
+
const factors = {
|
|
87
|
+
kb: 1000,
|
|
88
|
+
kib: 1024,
|
|
89
|
+
mb: 1000 * 1000,
|
|
90
|
+
mib: 1024 * 1024,
|
|
91
|
+
gb: 1000 * 1000 * 1000,
|
|
92
|
+
gib: 1024 * 1024 * 1024,
|
|
93
|
+
};
|
|
94
|
+
return Math.round(value * (factors[unit] || 1));
|
|
95
|
+
}
|
|
96
|
+
function parsePayloadLimit(text) {
|
|
97
|
+
const match = String(text || "").match(/(?:limit|max(?:imum)?(?:\s+body)?(?:\s+size)?)[^0-9]{0,12}(\d+(?:\.\d+)?\s*(?:bytes?|kib|kb|mib|mb|gib|gb))/i);
|
|
98
|
+
if (match)
|
|
99
|
+
return parseByteSize(match[1]);
|
|
100
|
+
return parseByteSize(text);
|
|
101
|
+
}
|
|
102
|
+
function findGitRoot(startDir) {
|
|
103
|
+
let current = path_1.default.resolve(startDir);
|
|
104
|
+
while (true) {
|
|
105
|
+
const candidate = path_1.default.join(current, ".git");
|
|
106
|
+
if (fs_1.default.existsSync(candidate))
|
|
107
|
+
return current;
|
|
108
|
+
const parent = path_1.default.dirname(current);
|
|
109
|
+
if (parent === current)
|
|
110
|
+
return null;
|
|
111
|
+
current = parent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
35
114
|
function getPayloadMessage(payload) {
|
|
36
115
|
if (!payload || typeof payload !== "object")
|
|
37
116
|
return "";
|
|
@@ -122,6 +201,162 @@ function resolveToken(tokenValue, projectId) {
|
|
|
122
201
|
}
|
|
123
202
|
return tokenValue;
|
|
124
203
|
}
|
|
204
|
+
function resolveLocalBin(binName) {
|
|
205
|
+
const baseName = process.platform === "win32" ? `${binName}.cmd` : binName;
|
|
206
|
+
const localBin = path_1.default.resolve(__dirname, "..", "node_modules", ".bin", baseName);
|
|
207
|
+
if (fs_1.default.existsSync(localBin))
|
|
208
|
+
return localBin;
|
|
209
|
+
return binName;
|
|
210
|
+
}
|
|
211
|
+
function parseJsonLoose(text) {
|
|
212
|
+
const raw = String(text || "").trim();
|
|
213
|
+
if (!raw)
|
|
214
|
+
return null;
|
|
215
|
+
const direct = parseJsonMaybe(raw);
|
|
216
|
+
if (direct !== null)
|
|
217
|
+
return direct;
|
|
218
|
+
const lines = raw
|
|
219
|
+
.split(/\r?\n/)
|
|
220
|
+
.map((line) => line.trim())
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
223
|
+
const parsed = parseJsonMaybe(lines[i]);
|
|
224
|
+
if (parsed !== null)
|
|
225
|
+
return parsed;
|
|
226
|
+
}
|
|
227
|
+
const firstBrace = raw.indexOf("{");
|
|
228
|
+
const firstBracket = raw.indexOf("[");
|
|
229
|
+
const starts = [firstBrace, firstBracket].filter((n) => n >= 0);
|
|
230
|
+
if (!starts.length)
|
|
231
|
+
return null;
|
|
232
|
+
const start = Math.min(...starts);
|
|
233
|
+
const lastBrace = raw.lastIndexOf("}");
|
|
234
|
+
const lastBracket = raw.lastIndexOf("]");
|
|
235
|
+
const end = Math.max(lastBrace, lastBracket);
|
|
236
|
+
if (end <= start)
|
|
237
|
+
return null;
|
|
238
|
+
const sliced = raw.slice(start, end + 1);
|
|
239
|
+
return parseJsonMaybe(sliced);
|
|
240
|
+
}
|
|
241
|
+
function extractCookiesFromPayload(payload) {
|
|
242
|
+
const queue = [payload];
|
|
243
|
+
const visited = new Set();
|
|
244
|
+
while (queue.length) {
|
|
245
|
+
const current = queue.shift();
|
|
246
|
+
if (current === null || current === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
if (visited.has(current))
|
|
249
|
+
continue;
|
|
250
|
+
visited.add(current);
|
|
251
|
+
if (Array.isArray(current)) {
|
|
252
|
+
if (current.every((item) => item && typeof item === "object")) {
|
|
253
|
+
return current;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (typeof current !== "object")
|
|
258
|
+
continue;
|
|
259
|
+
const obj = current;
|
|
260
|
+
if (Array.isArray(obj.cookies))
|
|
261
|
+
return obj.cookies;
|
|
262
|
+
if (Array.isArray(obj.data))
|
|
263
|
+
return obj.data;
|
|
264
|
+
if (obj.data && typeof obj.data === "object")
|
|
265
|
+
queue.push(obj.data);
|
|
266
|
+
if (obj.result && typeof obj.result === "object")
|
|
267
|
+
queue.push(obj.result);
|
|
268
|
+
if (obj.payload && typeof obj.payload === "object")
|
|
269
|
+
queue.push(obj.payload);
|
|
270
|
+
}
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
function normalizeCookieExpiresMs(raw) {
|
|
274
|
+
const n = Number(raw);
|
|
275
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
276
|
+
return undefined;
|
|
277
|
+
return n > 1_000_000_000_000 ? n : n * 1000;
|
|
278
|
+
}
|
|
279
|
+
function runAgentBrowser(args, options = {}) {
|
|
280
|
+
const bins = [
|
|
281
|
+
resolveLocalBin("agent-browser-stealth"),
|
|
282
|
+
resolveLocalBin("agent-browser"),
|
|
283
|
+
"agent-browser-stealth",
|
|
284
|
+
"agent-browser",
|
|
285
|
+
];
|
|
286
|
+
const uniq = Array.from(new Set(bins));
|
|
287
|
+
let lastError = null;
|
|
288
|
+
for (const bin of uniq) {
|
|
289
|
+
try {
|
|
290
|
+
if (options.captureOutput) {
|
|
291
|
+
const output = (0, child_process_1.execFileSync)(bin, args, {
|
|
292
|
+
encoding: "utf8",
|
|
293
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
294
|
+
});
|
|
295
|
+
return String(output || "");
|
|
296
|
+
}
|
|
297
|
+
(0, child_process_1.execFileSync)(bin, args, { stdio: "inherit" });
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
const code = error.code;
|
|
302
|
+
if (code === "ENOENT") {
|
|
303
|
+
lastError = error;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (options.ignoreError)
|
|
307
|
+
return "";
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (lastError?.code === "ENOENT") {
|
|
312
|
+
throw new Error("未找到 agent-browser-stealth。请先执行: pnpm -C packages/yapi-mcp add agent-browser-stealth && pnpm -C packages/yapi-mcp exec agent-browser-stealth install");
|
|
313
|
+
}
|
|
314
|
+
throw new Error("启动 agent-browser-stealth 失败");
|
|
315
|
+
}
|
|
316
|
+
async function loginByBrowserAndReadCookie(baseUrl, loginUrlOption) {
|
|
317
|
+
const normalizedBase = String(baseUrl || "").replace(/\/+$/, "");
|
|
318
|
+
const defaultLoginUrl = normalizedBase;
|
|
319
|
+
let loginUrl = String(loginUrlOption || "").trim();
|
|
320
|
+
if (!loginUrl) {
|
|
321
|
+
const answer = await promptText(`YApi page URL [${defaultLoginUrl}]: `);
|
|
322
|
+
loginUrl = answer.trim() || defaultLoginUrl;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const parsed = new URL(loginUrl);
|
|
326
|
+
loginUrl = parsed.toString();
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
throw new Error(`invalid login url: ${loginUrl}`);
|
|
330
|
+
}
|
|
331
|
+
const sessionName = `yapi-login-${Date.now()}-${crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
332
|
+
console.log(`opening browser for login: ${loginUrl}`);
|
|
333
|
+
console.log(`browser session name: ${sessionName}`);
|
|
334
|
+
runAgentBrowser(["open", loginUrl, "--headed", "--session-name", sessionName]);
|
|
335
|
+
await promptText("请在浏览器完成登录,然后按回车继续...");
|
|
336
|
+
let cookies = [];
|
|
337
|
+
try {
|
|
338
|
+
const output = runAgentBrowser(["cookies", "--json", "--session-name", sessionName], {
|
|
339
|
+
captureOutput: true,
|
|
340
|
+
});
|
|
341
|
+
const payload = parseJsonLoose(output);
|
|
342
|
+
cookies = extractCookiesFromPayload(payload);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
runAgentBrowser(["close", "--session-name", sessionName], { ignoreError: true });
|
|
346
|
+
}
|
|
347
|
+
if (!cookies.length) {
|
|
348
|
+
throw new Error(`未读取到浏览器 cookie。请确认登录完成后再回车;如果仍失败,请先自检:\nagent-browser-stealth cookies --json --session-name ${sessionName}`);
|
|
349
|
+
}
|
|
350
|
+
const tokenCookie = cookies.find((item) => String(item?.name || "") === "_yapi_token");
|
|
351
|
+
const uidCookie = cookies.find((item) => String(item?.name || "") === "_yapi_uid");
|
|
352
|
+
const yapiToken = String(tokenCookie?.value || "").trim();
|
|
353
|
+
if (!yapiToken) {
|
|
354
|
+
throw new Error("未找到 _yapi_token,请确认登录站点是目标 YApi 域名");
|
|
355
|
+
}
|
|
356
|
+
const yapiUid = String(uidCookie?.value || "").trim() || undefined;
|
|
357
|
+
const expiresAt = normalizeCookieExpiresMs(tokenCookie?.expiresAt) ?? normalizeCookieExpiresMs(tokenCookie?.expires);
|
|
358
|
+
return { yapiToken, yapiUid, expiresAt };
|
|
359
|
+
}
|
|
125
360
|
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
126
361
|
const controller = new AbortController();
|
|
127
362
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -184,6 +419,14 @@ function parseArgs(argv) {
|
|
|
184
419
|
options.baseUrl = arg.slice(11);
|
|
185
420
|
continue;
|
|
186
421
|
}
|
|
422
|
+
if (arg === "--login-url") {
|
|
423
|
+
options.loginUrl = argv[++i];
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (arg.startsWith("--login-url=")) {
|
|
427
|
+
options.loginUrl = arg.slice(12);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
187
430
|
if (arg === "--token") {
|
|
188
431
|
options.token = argv[++i];
|
|
189
432
|
continue;
|
|
@@ -208,6 +451,10 @@ function parseArgs(argv) {
|
|
|
208
451
|
options.authMode = arg.slice(12);
|
|
209
452
|
continue;
|
|
210
453
|
}
|
|
454
|
+
if (arg === "--browser") {
|
|
455
|
+
options.browser = true;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
211
458
|
if (arg === "--email") {
|
|
212
459
|
options.email = argv[++i];
|
|
213
460
|
continue;
|
|
@@ -640,6 +887,7 @@ function usage() {
|
|
|
640
887
|
" yapi docs-sync [options] [dir...]",
|
|
641
888
|
" yapi docs-sync bind <action> [options]",
|
|
642
889
|
" yapi login [options]",
|
|
890
|
+
" yapi logout [options]",
|
|
643
891
|
" yapi whoami [options]",
|
|
644
892
|
" yapi search [options]",
|
|
645
893
|
" yapi install-skill [options]",
|
|
@@ -740,12 +988,24 @@ function loginUsage() {
|
|
|
740
988
|
"Options:",
|
|
741
989
|
" --config <path> config file path (default: ~/.yapi/config.toml)",
|
|
742
990
|
" --base-url <url> YApi base URL",
|
|
991
|
+
" --login-url <url> page URL for browser login (default: <base-url>)",
|
|
992
|
+
" --browser force browser login and cookie sync",
|
|
743
993
|
" --email <email> login email for global mode",
|
|
744
994
|
" --password <pwd> login password for global mode",
|
|
745
995
|
" --timeout <ms> request timeout in ms",
|
|
746
996
|
" -h, --help show help",
|
|
747
997
|
].join("\n");
|
|
748
998
|
}
|
|
999
|
+
function logoutUsage() {
|
|
1000
|
+
return [
|
|
1001
|
+
"Usage:",
|
|
1002
|
+
" yapi logout [options]",
|
|
1003
|
+
"Options:",
|
|
1004
|
+
" --config <path> config file path (default: ~/.yapi/config.toml)",
|
|
1005
|
+
" --base-url <url> YApi base URL",
|
|
1006
|
+
" -h, --help show help",
|
|
1007
|
+
].join("\n");
|
|
1008
|
+
}
|
|
749
1009
|
function whoamiUsage() {
|
|
750
1010
|
return [
|
|
751
1011
|
"Usage:",
|
|
@@ -1141,6 +1401,8 @@ async function runLogin(rawArgs) {
|
|
|
1141
1401
|
let baseUrl = options.baseUrl || config.base_url || "";
|
|
1142
1402
|
let email = options.email || config.email || "";
|
|
1143
1403
|
let password = options.password || config.password || "";
|
|
1404
|
+
const projectId = options.projectId || config.project_id || "";
|
|
1405
|
+
const token = options.token || config.token || "";
|
|
1144
1406
|
let updated = false;
|
|
1145
1407
|
if (!baseUrl) {
|
|
1146
1408
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -1150,21 +1412,50 @@ async function runLogin(rawArgs) {
|
|
|
1150
1412
|
baseUrl = await promptRequired("YApi base URL: ", false);
|
|
1151
1413
|
updated = true;
|
|
1152
1414
|
}
|
|
1153
|
-
|
|
1415
|
+
const useBrowserLogin = Boolean(options.browser) || !email || !password;
|
|
1416
|
+
if (useBrowserLogin) {
|
|
1154
1417
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1155
|
-
console.error("
|
|
1418
|
+
console.error("browser login requires interactive terminal");
|
|
1156
1419
|
return 2;
|
|
1157
1420
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1421
|
+
try {
|
|
1422
|
+
const session = await loginByBrowserAndReadCookie(baseUrl, options.loginUrl);
|
|
1423
|
+
const cache = new authCache_1.YApiAuthCache(baseUrl, "warn");
|
|
1424
|
+
cache.saveSession({
|
|
1425
|
+
yapiToken: session.yapiToken,
|
|
1426
|
+
yapiUid: session.yapiUid,
|
|
1427
|
+
expiresAt: session.expiresAt,
|
|
1428
|
+
updatedAt: Date.now(),
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1164
1433
|
return 2;
|
|
1165
1434
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1435
|
+
const shouldWriteConfig = updated || !fs_1.default.existsSync(configPath) || config.auth_mode !== "global";
|
|
1436
|
+
if (shouldWriteConfig) {
|
|
1437
|
+
const mergedConfig = {
|
|
1438
|
+
base_url: baseUrl,
|
|
1439
|
+
auth_mode: "global",
|
|
1440
|
+
email,
|
|
1441
|
+
password,
|
|
1442
|
+
token,
|
|
1443
|
+
project_id: projectId,
|
|
1444
|
+
};
|
|
1445
|
+
writeConfig(configPath, mergedConfig);
|
|
1446
|
+
}
|
|
1447
|
+
console.log("login success (cookie synced from browser to ~/.yapi-mcp/auth-*.json)");
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1450
|
+
try {
|
|
1451
|
+
const authService = new auth_1.YApiAuthService(baseUrl, email, password, "warn", {
|
|
1452
|
+
timeoutMs: options.timeout || 30000,
|
|
1453
|
+
});
|
|
1454
|
+
await authService.getCookieHeaderWithLogin({ forceLogin: true });
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1458
|
+
return 2;
|
|
1168
1459
|
}
|
|
1169
1460
|
const shouldWriteConfig = updated || !fs_1.default.existsSync(configPath) || config.auth_mode !== "global";
|
|
1170
1461
|
if (shouldWriteConfig) {
|
|
@@ -1173,22 +1464,36 @@ async function runLogin(rawArgs) {
|
|
|
1173
1464
|
auth_mode: "global",
|
|
1174
1465
|
email,
|
|
1175
1466
|
password,
|
|
1176
|
-
token
|
|
1177
|
-
project_id:
|
|
1467
|
+
token,
|
|
1468
|
+
project_id: projectId,
|
|
1178
1469
|
};
|
|
1179
1470
|
writeConfig(configPath, mergedConfig);
|
|
1180
1471
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1472
|
+
console.log("login success (cookie cached in ~/.yapi-mcp/auth-*.json)");
|
|
1473
|
+
return 0;
|
|
1474
|
+
}
|
|
1475
|
+
async function runLogout(rawArgs) {
|
|
1476
|
+
const options = parseArgs(rawArgs);
|
|
1477
|
+
if (options.help) {
|
|
1478
|
+
console.log(logoutUsage());
|
|
1479
|
+
return 0;
|
|
1186
1480
|
}
|
|
1187
|
-
|
|
1188
|
-
console.
|
|
1481
|
+
if (options.version) {
|
|
1482
|
+
console.log(readVersion());
|
|
1483
|
+
return 0;
|
|
1484
|
+
}
|
|
1485
|
+
const configPath = options.config || globalConfigPath();
|
|
1486
|
+
const config = fs_1.default.existsSync(configPath)
|
|
1487
|
+
? parseSimpleToml(fs_1.default.readFileSync(configPath, "utf8"))
|
|
1488
|
+
: {};
|
|
1489
|
+
const baseUrl = options.baseUrl || config.base_url || "";
|
|
1490
|
+
if (!baseUrl) {
|
|
1491
|
+
console.error("missing --base-url or config base_url");
|
|
1189
1492
|
return 2;
|
|
1190
1493
|
}
|
|
1191
|
-
|
|
1494
|
+
const cache = new authCache_1.YApiAuthCache(baseUrl, "warn");
|
|
1495
|
+
cache.clearSession();
|
|
1496
|
+
console.log("logout success (session cleared from ~/.yapi-mcp/auth-*.json)");
|
|
1192
1497
|
return 0;
|
|
1193
1498
|
}
|
|
1194
1499
|
async function runWhoami(rawArgs) {
|
|
@@ -1477,6 +1782,20 @@ function resolveDocsSyncHome(startDir, ensure) {
|
|
|
1477
1782
|
ensureDocsSyncReadme(home);
|
|
1478
1783
|
return home;
|
|
1479
1784
|
}
|
|
1785
|
+
function globalYapiHomeDir() {
|
|
1786
|
+
return path_1.default.resolve(process.env.YAPI_HOME || path_1.default.join(os_1.default.homedir(), ".yapi"));
|
|
1787
|
+
}
|
|
1788
|
+
function normalizeComparablePath(targetPath) {
|
|
1789
|
+
try {
|
|
1790
|
+
return fs_1.default.realpathSync.native(targetPath);
|
|
1791
|
+
}
|
|
1792
|
+
catch {
|
|
1793
|
+
return path_1.default.resolve(targetPath);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function isGlobalDocsSyncHome(homeDir) {
|
|
1797
|
+
return normalizeComparablePath(homeDir) === normalizeComparablePath(globalYapiHomeDir());
|
|
1798
|
+
}
|
|
1480
1799
|
function docsSyncConfigPath(homeDir) {
|
|
1481
1800
|
return path_1.default.join(homeDir, "docs-sync.json");
|
|
1482
1801
|
}
|
|
@@ -1591,6 +1910,44 @@ function normalizeBindingDir(rootDir, bindingDir) {
|
|
|
1591
1910
|
return resolved;
|
|
1592
1911
|
return relative;
|
|
1593
1912
|
}
|
|
1913
|
+
function getBindingBaseDir(homeDir, rootDir, cwd) {
|
|
1914
|
+
if (!isGlobalDocsSyncHome(homeDir)) {
|
|
1915
|
+
return { baseDir: rootDir, gitRoot: findGitRoot(cwd), usedGitRoot: false };
|
|
1916
|
+
}
|
|
1917
|
+
const gitRoot = findGitRoot(cwd);
|
|
1918
|
+
if (gitRoot) {
|
|
1919
|
+
return { baseDir: gitRoot, gitRoot, usedGitRoot: true };
|
|
1920
|
+
}
|
|
1921
|
+
return { baseDir: path_1.default.resolve(cwd), gitRoot: null, usedGitRoot: false };
|
|
1922
|
+
}
|
|
1923
|
+
function normalizeBindingDirForContext(homeDir, rootDir, cwd, bindingDir) {
|
|
1924
|
+
const context = getBindingBaseDir(homeDir, rootDir, cwd);
|
|
1925
|
+
const resolved = path_1.default.isAbsolute(bindingDir)
|
|
1926
|
+
? bindingDir
|
|
1927
|
+
: path_1.default.resolve(context.baseDir, bindingDir);
|
|
1928
|
+
const relative = path_1.default.relative(rootDir, resolved);
|
|
1929
|
+
if (!relative || relative === ".")
|
|
1930
|
+
return ".";
|
|
1931
|
+
if (relative.startsWith("..") || path_1.default.isAbsolute(relative))
|
|
1932
|
+
return resolved;
|
|
1933
|
+
return relative;
|
|
1934
|
+
}
|
|
1935
|
+
function resolveBindingDirForContext(homeDir, rootDir, cwd, bindingDir) {
|
|
1936
|
+
if (!bindingDir)
|
|
1937
|
+
return rootDir;
|
|
1938
|
+
if (path_1.default.isAbsolute(bindingDir))
|
|
1939
|
+
return bindingDir;
|
|
1940
|
+
const direct = path_1.default.resolve(rootDir, bindingDir);
|
|
1941
|
+
if (!isGlobalDocsSyncHome(homeDir))
|
|
1942
|
+
return direct;
|
|
1943
|
+
if (fs_1.default.existsSync(direct))
|
|
1944
|
+
return direct;
|
|
1945
|
+
const { baseDir } = getBindingBaseDir(homeDir, rootDir, cwd);
|
|
1946
|
+
const contextual = path_1.default.resolve(baseDir, bindingDir);
|
|
1947
|
+
if (fs_1.default.existsSync(contextual))
|
|
1948
|
+
return contextual;
|
|
1949
|
+
return direct;
|
|
1950
|
+
}
|
|
1594
1951
|
function suggestDocsSyncDir(startDir) {
|
|
1595
1952
|
const candidates = ["docs", "doc", "documentation", "release-notes"];
|
|
1596
1953
|
for (const candidate of candidates) {
|
|
@@ -1868,6 +2225,59 @@ function buildAddPayload(template, title, apiPath, catId, projectId) {
|
|
|
1868
2225
|
tag: template.tag || [],
|
|
1869
2226
|
};
|
|
1870
2227
|
}
|
|
2228
|
+
function buildUpdatePayload(docId, title, markdown, html) {
|
|
2229
|
+
const payload = { id: docId, markdown, desc: html };
|
|
2230
|
+
if (title) {
|
|
2231
|
+
payload.title = title;
|
|
2232
|
+
}
|
|
2233
|
+
return payload;
|
|
2234
|
+
}
|
|
2235
|
+
function pickLargestMermaid(metrics) {
|
|
2236
|
+
return metrics
|
|
2237
|
+
.filter((item) => item.renderer === "mermaid")
|
|
2238
|
+
.sort((a, b) => b.renderedBytes - a.renderedBytes)[0];
|
|
2239
|
+
}
|
|
2240
|
+
function buildDocsSyncPreviewLine(item) {
|
|
2241
|
+
const parts = [
|
|
2242
|
+
`file=${item.fileName}`,
|
|
2243
|
+
`action=${item.action}`,
|
|
2244
|
+
`markdown=${formatBytes(item.markdownBytes)}`,
|
|
2245
|
+
`html=${formatBytes(item.htmlBytes)}`,
|
|
2246
|
+
`payload=${formatBytes(item.payloadBytes)}`,
|
|
2247
|
+
`path=${item.apiPath}`,
|
|
2248
|
+
];
|
|
2249
|
+
if (item.docId) {
|
|
2250
|
+
parts.push(`doc_id=${item.docId}`);
|
|
2251
|
+
}
|
|
2252
|
+
if (item.largestMermaid) {
|
|
2253
|
+
parts.push(`largest_mermaid=#${item.largestMermaid.index}`, `largest_mermaid_svg=${formatBytes(item.largestMermaid.renderedBytes)}`);
|
|
2254
|
+
}
|
|
2255
|
+
return `preview ${parts.join(" ")}`;
|
|
2256
|
+
}
|
|
2257
|
+
function buildDocsSyncPayloadTooLargeMessage(fileName, preview, error) {
|
|
2258
|
+
const lines = [
|
|
2259
|
+
`413 Payload Too Large while syncing ${fileName}`,
|
|
2260
|
+
`- request payload: ${formatBytes(preview.payloadBytes)}`,
|
|
2261
|
+
`- markdown size: ${formatBytes(preview.markdownBytes)}`,
|
|
2262
|
+
`- rendered html size: ${formatBytes(preview.htmlBytes)}`,
|
|
2263
|
+
];
|
|
2264
|
+
const limitBytes = parsePayloadLimit(error.body || error.message);
|
|
2265
|
+
if (limitBytes) {
|
|
2266
|
+
lines.push(`- server limit: ${formatBytes(limitBytes)}`);
|
|
2267
|
+
}
|
|
2268
|
+
else {
|
|
2269
|
+
lines.push("- server limit: unknown (response did not expose an exact value)");
|
|
2270
|
+
}
|
|
2271
|
+
if (preview.largestMermaid) {
|
|
2272
|
+
lines.push(`- largest Mermaid block: #${preview.largestMermaid.index} -> ${formatBytes(preview.largestMermaid.renderedBytes)}`);
|
|
2273
|
+
}
|
|
2274
|
+
else {
|
|
2275
|
+
lines.push("- largest Mermaid block: none");
|
|
2276
|
+
}
|
|
2277
|
+
lines.push("- suggestion: run `yapi docs-sync --dry-run ...` to preview all files before upload");
|
|
2278
|
+
lines.push("- suggestion: split oversized Mermaid diagrams or move them into separate docs");
|
|
2279
|
+
return lines.join("\n");
|
|
2280
|
+
}
|
|
1871
2281
|
async function addInterface(title, apiPath, mapping, request) {
|
|
1872
2282
|
const projectId = Number(mapping.project_id || 0);
|
|
1873
2283
|
const catId = Number(mapping.catid || 0);
|
|
@@ -1894,10 +2304,7 @@ async function addInterface(title, apiPath, mapping, request) {
|
|
|
1894
2304
|
return Number(newId);
|
|
1895
2305
|
}
|
|
1896
2306
|
async function updateInterface(docId, title, markdown, html, request) {
|
|
1897
|
-
const payload =
|
|
1898
|
-
if (title) {
|
|
1899
|
-
payload.title = title;
|
|
1900
|
-
}
|
|
2307
|
+
const payload = buildUpdatePayload(docId, title, markdown, html);
|
|
1901
2308
|
const resp = await request("/api/interface/up", "POST", {}, payload);
|
|
1902
2309
|
if (resp?.errcode !== 0) {
|
|
1903
2310
|
throw new Error(`interface up failed: ${resp?.errmsg || "unknown error"}`);
|
|
@@ -1919,14 +2326,19 @@ async function syncDocsDir(dirPath, mapping, options, request) {
|
|
|
1919
2326
|
mapping.catid = Number(envCatId);
|
|
1920
2327
|
if (!mapping.template_id && envTemplateId)
|
|
1921
2328
|
mapping.template_id = Number(envTemplateId);
|
|
1922
|
-
|
|
2329
|
+
const hasTarget = Boolean(mapping.project_id && mapping.catid);
|
|
2330
|
+
if (!hasTarget && !options.dryRun) {
|
|
1923
2331
|
throw new Error("缺少 project_id/catid。请先绑定或配置:yapi docs-sync bind add --name <binding> --dir <path> --project-id <id> --catid <id>,或在目录下添加 .yapi.json,或设置环境变量 YAPI_PROJECT_ID/YAPI_CATID。");
|
|
1924
2332
|
}
|
|
1925
|
-
const { byPath, byTitle, byId } =
|
|
2333
|
+
const { byPath, byTitle, byId } = hasTarget
|
|
2334
|
+
? await listExistingInterfaces(Number(mapping.catid), request)
|
|
2335
|
+
: { byPath: {}, byTitle: {}, byId: {} };
|
|
1926
2336
|
let updated = 0;
|
|
1927
2337
|
let created = 0;
|
|
1928
2338
|
let skipped = 0;
|
|
2339
|
+
let previewOnly = 0;
|
|
1929
2340
|
const fileInfos = {};
|
|
2341
|
+
const previews = [];
|
|
1930
2342
|
const files = resolveSourceFiles(dirPath, mapping);
|
|
1931
2343
|
for (const mdPath of files) {
|
|
1932
2344
|
const stem = path_1.default.parse(mdPath).name;
|
|
@@ -1940,43 +2352,34 @@ async function syncDocsDir(dirPath, mapping, options, request) {
|
|
|
1940
2352
|
if (docId)
|
|
1941
2353
|
mapping.files[relName] = docId;
|
|
1942
2354
|
}
|
|
1943
|
-
|
|
2355
|
+
let action = docId ? "update" : hasTarget ? "create" : "preview-only";
|
|
2356
|
+
if (!docId && hasTarget) {
|
|
1944
2357
|
created += 1;
|
|
1945
2358
|
if (!options.dryRun) {
|
|
1946
2359
|
docId = await addInterface(desiredTitle, apiPath, mapping, request);
|
|
1947
2360
|
mapping.files[relName] = docId;
|
|
1948
2361
|
}
|
|
1949
2362
|
}
|
|
2363
|
+
if (!docId && !hasTarget) {
|
|
2364
|
+
previewOnly += 1;
|
|
2365
|
+
}
|
|
1950
2366
|
if (docId) {
|
|
1951
2367
|
const resolvedPath = byId[String(docId)]?.path || apiPath;
|
|
1952
2368
|
fileInfos[relName] = { docId: Number(docId), apiPath: resolvedPath };
|
|
1953
2369
|
}
|
|
1954
|
-
const contentHash = buildDocsSyncHash(markdown, options);
|
|
1955
|
-
const previousHash = mapping.file_hashes[relName];
|
|
1956
|
-
const currentTitle = docId ? byId[String(docId)]?.title : "";
|
|
1957
|
-
const titleToUpdate = !docId
|
|
1958
|
-
? undefined
|
|
1959
|
-
: !currentTitle || currentTitle !== desiredTitle
|
|
1960
|
-
? desiredTitle
|
|
1961
|
-
: undefined;
|
|
1962
|
-
const shouldSyncTitle = Boolean(titleToUpdate);
|
|
1963
|
-
if (!options.force &&
|
|
1964
|
-
docId &&
|
|
1965
|
-
previousHash &&
|
|
1966
|
-
previousHash === contentHash &&
|
|
1967
|
-
!shouldSyncTitle) {
|
|
1968
|
-
skipped += 1;
|
|
1969
|
-
continue;
|
|
1970
|
-
}
|
|
1971
2370
|
const logPrefix = `[docs-sync:${relName}]`;
|
|
1972
2371
|
let mermaidFailed = false;
|
|
1973
2372
|
let diagramFailed = false;
|
|
2373
|
+
const diagramMetrics = [];
|
|
1974
2374
|
const html = (0, markdown_1.renderMarkdownToHtml)(markdown, {
|
|
1975
2375
|
noMermaid: options.noMermaid,
|
|
1976
2376
|
logMermaid: true,
|
|
1977
2377
|
mermaidLook: options.mermaidLook,
|
|
1978
2378
|
mermaidHandDrawnSeed: options.mermaidHandDrawnSeed,
|
|
1979
2379
|
logger: (message) => console.log(`${logPrefix} ${message}`),
|
|
2380
|
+
onDiagramRendered: (metric) => {
|
|
2381
|
+
diagramMetrics.push(metric);
|
|
2382
|
+
},
|
|
1980
2383
|
onMermaidError: () => {
|
|
1981
2384
|
mermaidFailed = true;
|
|
1982
2385
|
},
|
|
@@ -1984,15 +2387,56 @@ async function syncDocsDir(dirPath, mapping, options, request) {
|
|
|
1984
2387
|
diagramFailed = true;
|
|
1985
2388
|
},
|
|
1986
2389
|
});
|
|
1987
|
-
|
|
1988
|
-
|
|
2390
|
+
const contentHash = buildDocsSyncHash(markdown, options);
|
|
2391
|
+
const previousHash = mapping.file_hashes[relName];
|
|
2392
|
+
const currentTitle = docId ? byId[String(docId)]?.title : "";
|
|
2393
|
+
const titleToUpdate = !docId
|
|
2394
|
+
? undefined
|
|
2395
|
+
: !currentTitle || currentTitle !== desiredTitle
|
|
2396
|
+
? desiredTitle
|
|
2397
|
+
: undefined;
|
|
2398
|
+
const shouldSyncTitle = Boolean(titleToUpdate);
|
|
2399
|
+
if (!options.force &&
|
|
2400
|
+
docId &&
|
|
2401
|
+
previousHash &&
|
|
2402
|
+
previousHash === contentHash &&
|
|
2403
|
+
!shouldSyncTitle) {
|
|
2404
|
+
action = "skip";
|
|
2405
|
+
skipped += 1;
|
|
2406
|
+
}
|
|
2407
|
+
const payloadObject = docId && action !== "create"
|
|
2408
|
+
? buildUpdatePayload(docId, titleToUpdate, markdown, html)
|
|
2409
|
+
: buildAddPayload({}, desiredTitle, apiPath, Number(mapping.catid || 0), Number(mapping.project_id || 0));
|
|
2410
|
+
const preview = {
|
|
2411
|
+
fileName: relName,
|
|
2412
|
+
action,
|
|
2413
|
+
markdownBytes: Buffer.byteLength(markdown, "utf8"),
|
|
2414
|
+
htmlBytes: Buffer.byteLength(html, "utf8"),
|
|
2415
|
+
payloadBytes: Buffer.byteLength(JSON.stringify(payloadObject), "utf8"),
|
|
2416
|
+
apiPath: docId ? fileInfos[relName]?.apiPath || apiPath : apiPath,
|
|
2417
|
+
docId: docId ? Number(docId) : undefined,
|
|
2418
|
+
largestMermaid: pickLargestMermaid(diagramMetrics),
|
|
2419
|
+
};
|
|
2420
|
+
previews.push(preview);
|
|
2421
|
+
if (!options.dryRun && docId && action !== "skip") {
|
|
2422
|
+
try {
|
|
2423
|
+
await updateInterface(docId, titleToUpdate, markdown, html, request);
|
|
2424
|
+
}
|
|
2425
|
+
catch (error) {
|
|
2426
|
+
if (error instanceof HttpStatusError && error.status === 413) {
|
|
2427
|
+
throw new Error(buildDocsSyncPayloadTooLargeMessage(relName, preview, error));
|
|
2428
|
+
}
|
|
2429
|
+
throw error;
|
|
2430
|
+
}
|
|
1989
2431
|
}
|
|
1990
2432
|
if (docId && !mermaidFailed && !diagramFailed) {
|
|
1991
2433
|
mapping.file_hashes[relName] = contentHash;
|
|
1992
2434
|
}
|
|
1993
|
-
|
|
2435
|
+
if (action !== "skip") {
|
|
2436
|
+
updated += 1;
|
|
2437
|
+
}
|
|
1994
2438
|
}
|
|
1995
|
-
return { updated, created, skipped, files: fileInfos };
|
|
2439
|
+
return { updated, created, skipped, previewOnly, files: fileInfos, previews };
|
|
1996
2440
|
}
|
|
1997
2441
|
function buildEnvUrls(projectInfo, apiPath) {
|
|
1998
2442
|
const urls = {};
|
|
@@ -2167,7 +2611,7 @@ async function runDocsSyncBindings(rawArgs) {
|
|
|
2167
2611
|
file_hashes: existing.file_hashes ? { ...existing.file_hashes } : {},
|
|
2168
2612
|
};
|
|
2169
2613
|
if (options.dir) {
|
|
2170
|
-
next.dir =
|
|
2614
|
+
next.dir = normalizeBindingDirForContext(homeDir, rootDir, process.cwd(), options.dir);
|
|
2171
2615
|
}
|
|
2172
2616
|
if (options.projectId !== undefined && Number.isFinite(options.projectId)) {
|
|
2173
2617
|
next.project_id = Number(options.projectId);
|
|
@@ -2190,7 +2634,17 @@ async function runDocsSyncBindings(rawArgs) {
|
|
|
2190
2634
|
}
|
|
2191
2635
|
config.bindings[options.name] = next;
|
|
2192
2636
|
saveDocsSyncConfig(homeDir, config);
|
|
2637
|
+
const resolvedDir = resolveBindingDirForContext(homeDir, rootDir, process.cwd(), next.dir);
|
|
2193
2638
|
console.log(`${action === "add" ? "binding added" : "binding updated"}: ${options.name}`);
|
|
2639
|
+
console.log(`stored_dir=${next.dir}`);
|
|
2640
|
+
console.log(`resolved_dir=${resolvedDir}`);
|
|
2641
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
2642
|
+
if (gitRoot) {
|
|
2643
|
+
console.log(`git_root=${gitRoot}`);
|
|
2644
|
+
}
|
|
2645
|
+
else if (isGlobalDocsSyncHome(homeDir)) {
|
|
2646
|
+
console.warn("warning: no git root detected, relative --dir was resolved from current working directory");
|
|
2647
|
+
}
|
|
2194
2648
|
return 0;
|
|
2195
2649
|
}
|
|
2196
2650
|
async function runDocsSync(rawArgs) {
|
|
@@ -2398,7 +2852,7 @@ async function runDocsSync(rawArgs) {
|
|
|
2398
2852
|
result = await sendOnce();
|
|
2399
2853
|
}
|
|
2400
2854
|
if (!result.response.ok) {
|
|
2401
|
-
throw new
|
|
2855
|
+
throw new HttpStatusError(endpoint, result.response.status, result.response.statusText, result.text);
|
|
2402
2856
|
}
|
|
2403
2857
|
if (!result.json) {
|
|
2404
2858
|
throw new Error(`invalid JSON response from ${endpoint}`);
|
|
@@ -2414,7 +2868,7 @@ async function runDocsSync(rawArgs) {
|
|
|
2414
2868
|
if (!binding) {
|
|
2415
2869
|
throw new Error(`binding not found: ${name}`);
|
|
2416
2870
|
}
|
|
2417
|
-
const dirPath =
|
|
2871
|
+
const dirPath = resolveBindingDirForContext(docsSyncHome, rootDir, process.cwd(), binding.dir);
|
|
2418
2872
|
const existing = dirToBindings.get(dirPath) || [];
|
|
2419
2873
|
existing.push(name);
|
|
2420
2874
|
dirToBindings.set(dirPath, existing);
|
|
@@ -2439,12 +2893,16 @@ async function runDocsSync(rawArgs) {
|
|
|
2439
2893
|
if (!binding) {
|
|
2440
2894
|
throw new Error(`binding not found: ${name}`);
|
|
2441
2895
|
}
|
|
2442
|
-
const dirPath =
|
|
2896
|
+
const dirPath = resolveBindingDirForContext(docsSyncHome, rootDir, process.cwd(), binding.dir);
|
|
2443
2897
|
if (!fs_1.default.existsSync(dirPath) || !fs_1.default.statSync(dirPath).isDirectory()) {
|
|
2444
2898
|
throw new Error(`dir not found for binding ${name}: ${dirPath}`);
|
|
2445
2899
|
}
|
|
2446
2900
|
const result = await syncDocsDir(dirPath, binding, options, request);
|
|
2447
|
-
|
|
2901
|
+
if (options.dryRun) {
|
|
2902
|
+
console.log(`dry-run preview binding=${name}`);
|
|
2903
|
+
result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
|
|
2904
|
+
}
|
|
2905
|
+
console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} binding=${name} dir=${dirPath}`);
|
|
2448
2906
|
bindingResults[name] = { binding, files: result.files };
|
|
2449
2907
|
}
|
|
2450
2908
|
if (!options.dryRun) {
|
|
@@ -2460,10 +2918,14 @@ async function runDocsSync(rawArgs) {
|
|
|
2460
2918
|
}
|
|
2461
2919
|
const { mapping, mappingPath } = loadMapping(dirPath);
|
|
2462
2920
|
const result = await syncDocsDir(dirPath, mapping, options, request);
|
|
2921
|
+
if (options.dryRun) {
|
|
2922
|
+
console.log(`dry-run preview dir=${dirPath}`);
|
|
2923
|
+
result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
|
|
2924
|
+
}
|
|
2463
2925
|
if (!options.dryRun) {
|
|
2464
2926
|
saveMapping(mapping, mappingPath);
|
|
2465
2927
|
}
|
|
2466
|
-
console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} dir=${dirPath}`);
|
|
2928
|
+
console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} dir=${dirPath}`);
|
|
2467
2929
|
}
|
|
2468
2930
|
}
|
|
2469
2931
|
return 0;
|
|
@@ -2489,6 +2951,9 @@ async function main() {
|
|
|
2489
2951
|
if (rawArgs[0] === "login") {
|
|
2490
2952
|
return await runLogin(rawArgs.slice(1));
|
|
2491
2953
|
}
|
|
2954
|
+
if (rawArgs[0] === "logout") {
|
|
2955
|
+
return await runLogout(rawArgs.slice(1));
|
|
2956
|
+
}
|
|
2492
2957
|
if (rawArgs[0] === "whoami") {
|
|
2493
2958
|
return await runWhoami(rawArgs.slice(1));
|
|
2494
2959
|
}
|
|
@@ -2631,7 +3096,7 @@ async function main() {
|
|
|
2631
3096
|
}
|
|
2632
3097
|
const queryItems = [];
|
|
2633
3098
|
for (const query of options.query || []) {
|
|
2634
|
-
queryItems.push(
|
|
3099
|
+
queryItems.push(...parseQueryArg(query));
|
|
2635
3100
|
}
|
|
2636
3101
|
const url = buildUrl(baseUrl, endpoint, queryItems, authMode === "token" ? token : "", options.tokenParam || "token");
|
|
2637
3102
|
let dataRaw = null;
|