bfi-cli 0.1.109 → 0.1.111
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/dist/_chunks/package-plugin.mjs +1 -1
- package/dist/cli.mjs +163 -12
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
8
|
import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import { z } from "zod";
|
|
11
10
|
import { execFile } from "node:child_process";
|
|
11
|
+
import { z } from "zod";
|
|
12
12
|
const nativeManifestFormats = {
|
|
13
13
|
"claude-code": ["CLAUDE"],
|
|
14
14
|
codex: [
|
package/dist/cli.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { promisify } from "util";
|
|
|
30
30
|
import { execFile, spawn, spawnSync } from "child_process";
|
|
31
31
|
import { access as access$1, chmod, cp as cp$1, lstat as lstat$1, mkdir as mkdir$1, mkdtemp, readFile as readFile$1, readdir as readdir$1, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
|
|
32
32
|
import { determineAgent } from "@vercel/detect-agent";
|
|
33
|
+
import { spawn as spawn$1 } from "node:child_process";
|
|
33
34
|
import { applyEdits, getNodeValue, modify, parseTree } from "jsonc-parser";
|
|
34
35
|
import { parse as parse$1, stringify } from "smol-toml";
|
|
35
36
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
@@ -5899,9 +5900,68 @@ function fail(message) {
|
|
|
5899
5900
|
process.exit(1);
|
|
5900
5901
|
}
|
|
5901
5902
|
var UseCommandError = class extends Error {};
|
|
5903
|
+
function openBrowser(url) {
|
|
5904
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
5905
|
+
const args = process.platform === "win32" ? [
|
|
5906
|
+
"/c",
|
|
5907
|
+
"start",
|
|
5908
|
+
"",
|
|
5909
|
+
url
|
|
5910
|
+
] : [url];
|
|
5911
|
+
try {
|
|
5912
|
+
const child = spawn$1(cmd, args, {
|
|
5913
|
+
detached: true,
|
|
5914
|
+
stdio: "ignore"
|
|
5915
|
+
});
|
|
5916
|
+
child.on("error", () => {});
|
|
5917
|
+
child.unref();
|
|
5918
|
+
} catch {}
|
|
5919
|
+
}
|
|
5920
|
+
var DeviceFlowUnavailableError = class extends Error {};
|
|
5921
|
+
var DeviceFlowDeniedError = class extends Error {};
|
|
5922
|
+
function dataOf(body) {
|
|
5923
|
+
if (body && typeof body === "object") {
|
|
5924
|
+
const data = body.data;
|
|
5925
|
+
if (data && typeof data === "object") return data;
|
|
5926
|
+
}
|
|
5927
|
+
return {};
|
|
5928
|
+
}
|
|
5929
|
+
async function requestDeviceCode(client) {
|
|
5930
|
+
const resp = await client.request("POST", "/auth/device/code");
|
|
5931
|
+
const data = dataOf(resp.body);
|
|
5932
|
+
const deviceCode = String(data.deviceCode || "");
|
|
5933
|
+
const userCode = String(data.userCode || "");
|
|
5934
|
+
const verificationUri = String(data.verificationUri || "");
|
|
5935
|
+
if (resp.status !== 200 || !deviceCode || !userCode || !verificationUri) throw new DeviceFlowUnavailableError(`设备授权接口不可用(HTTP ${resp.status})`);
|
|
5936
|
+
return {
|
|
5937
|
+
deviceCode,
|
|
5938
|
+
userCode,
|
|
5939
|
+
verificationUri,
|
|
5940
|
+
expiresIn: Number(data.expiresIn) > 0 ? Number(data.expiresIn) : 900,
|
|
5941
|
+
interval: Number(data.interval) > 0 ? Number(data.interval) : 5
|
|
5942
|
+
};
|
|
5943
|
+
}
|
|
5944
|
+
async function pollDeviceToken(client, deviceCode, opts) {
|
|
5945
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5946
|
+
const now = opts.now ?? (() => Date.now());
|
|
5947
|
+
const deadline = now() + opts.expiresInSec * 1e3;
|
|
5948
|
+
while (now() < deadline) {
|
|
5949
|
+
await sleep(opts.intervalSec * 1e3);
|
|
5950
|
+
const resp = await client.request("POST", "/auth/device/token", { body: { deviceCode } });
|
|
5951
|
+
const data = dataOf(resp.body);
|
|
5952
|
+
if (resp.status === 200 && data.accessToken) return String(data.accessToken);
|
|
5953
|
+
if (resp.status >= 500) throw new DeviceFlowUnavailableError(`设备授权轮询失败(HTTP ${resp.status})`);
|
|
5954
|
+
const error = String(data.error || "");
|
|
5955
|
+
if (error === "authorization_pending") continue;
|
|
5956
|
+
if (error) throw new DeviceFlowDeniedError(`设备授权失败:${error}`);
|
|
5957
|
+
}
|
|
5958
|
+
throw new DeviceFlowDeniedError("设备授权超时,请重试");
|
|
5959
|
+
}
|
|
5902
5960
|
async function runLogin(args, globalOpts) {
|
|
5903
|
-
const token = getOptionValue$3(args, "--token") || await promptForToken();
|
|
5904
5961
|
const { registry } = resolveConfig(globalOpts);
|
|
5962
|
+
const explicitToken = getOptionValue$3(args, "--token");
|
|
5963
|
+
const noDevice = args.includes("--no-device");
|
|
5964
|
+
const token = explicitToken || (noDevice ? await promptForToken() : await loginViaDevice(registry));
|
|
5905
5965
|
const api = new SkillHubAPI(registry, token);
|
|
5906
5966
|
try {
|
|
5907
5967
|
const response = await api.whoami();
|
|
@@ -5922,6 +5982,42 @@ async function runLogin(args, globalOpts) {
|
|
|
5922
5982
|
process.exit(1);
|
|
5923
5983
|
}
|
|
5924
5984
|
}
|
|
5985
|
+
async function loginViaDevice(registry) {
|
|
5986
|
+
const tokenPage = `${registry.replace(/\/$/, "")}/dashboard/tokens`;
|
|
5987
|
+
const client = new SkillHubAPI(registry, null);
|
|
5988
|
+
let deviceCode;
|
|
5989
|
+
try {
|
|
5990
|
+
deviceCode = await requestDeviceCode(client);
|
|
5991
|
+
} catch (error) {
|
|
5992
|
+
return fallbackToTokenInput(error, tokenPage);
|
|
5993
|
+
}
|
|
5994
|
+
console.error("\n请在浏览器打开以下地址完成授权:");
|
|
5995
|
+
console.error(` ${deviceCode.verificationUri}`);
|
|
5996
|
+
console.error(`并输入设备代码:${deviceCode.userCode}(${Math.ceil(deviceCode.expiresIn / 60)} 分钟内有效)`);
|
|
5997
|
+
openBrowser(deviceCode.verificationUri);
|
|
5998
|
+
console.error("等待授权中…(Ctrl+C 取消)");
|
|
5999
|
+
try {
|
|
6000
|
+
const token = await pollDeviceToken(client, deviceCode.deviceCode, {
|
|
6001
|
+
intervalSec: deviceCode.interval,
|
|
6002
|
+
expiresInSec: deviceCode.expiresIn
|
|
6003
|
+
});
|
|
6004
|
+
console.error("✓ 设备授权成功");
|
|
6005
|
+
return token;
|
|
6006
|
+
} catch (error) {
|
|
6007
|
+
if (error instanceof DeviceFlowUnavailableError) return fallbackToTokenInput(error, tokenPage);
|
|
6008
|
+
if (error instanceof DeviceFlowDeniedError) {
|
|
6009
|
+
console.error(`登录失败:${error.message}`);
|
|
6010
|
+
process.exit(1);
|
|
6011
|
+
}
|
|
6012
|
+
throw error;
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
async function fallbackToTokenInput(error, tokenPage) {
|
|
6016
|
+
console.error(`⚠️ ${error?.message || "设备授权不可用"},改用网页 Token。`);
|
|
6017
|
+
console.error(`Token 页面:${tokenPage}`);
|
|
6018
|
+
openBrowser(tokenPage);
|
|
6019
|
+
return promptForToken();
|
|
6020
|
+
}
|
|
5925
6021
|
async function runLogout() {
|
|
5926
6022
|
const config = loadGlobalConfig();
|
|
5927
6023
|
if (config?.token) {
|
|
@@ -5934,7 +6030,7 @@ async function runLogout() {
|
|
|
5934
6030
|
async function runWhoami(globalOpts) {
|
|
5935
6031
|
const { registry, token } = resolveConfig(globalOpts);
|
|
5936
6032
|
if (!token) {
|
|
5937
|
-
console.error("未认证。运行:bfi auth login
|
|
6033
|
+
console.error("未认证。运行:bfi auth login(浏览器设备授权;或 --token 直接指定)");
|
|
5938
6034
|
process.exit(1);
|
|
5939
6035
|
}
|
|
5940
6036
|
const response = await new SkillHubAPI(registry, token).whoami();
|
|
@@ -8992,7 +9088,8 @@ const ALLOWED_FLAGS = {
|
|
|
8992
9088
|
"--app",
|
|
8993
9089
|
"--no-profiler",
|
|
8994
9090
|
"--page",
|
|
8995
|
-
"--page-size"
|
|
9091
|
+
"--page-size",
|
|
9092
|
+
"--timeout"
|
|
8996
9093
|
],
|
|
8997
9094
|
slowcall: [
|
|
8998
9095
|
...F_ENV,
|
|
@@ -9003,7 +9100,8 @@ const ALLOWED_FLAGS = {
|
|
|
9003
9100
|
"--profiler",
|
|
9004
9101
|
"--min-cost",
|
|
9005
9102
|
"--page",
|
|
9006
|
-
"--page-size"
|
|
9103
|
+
"--page-size",
|
|
9104
|
+
"--timeout"
|
|
9007
9105
|
],
|
|
9008
9106
|
exceptions: [
|
|
9009
9107
|
...F_ENV,
|
|
@@ -9144,6 +9242,43 @@ function timeOpts(args) {
|
|
|
9144
9242
|
if (et) out.end_time = et;
|
|
9145
9243
|
return out;
|
|
9146
9244
|
}
|
|
9245
|
+
function slowQueryTimeoutMs(args) {
|
|
9246
|
+
const raw = opt(args, "--timeout");
|
|
9247
|
+
if (raw === void 0) return 15e4;
|
|
9248
|
+
const sec = Number(raw);
|
|
9249
|
+
if (!Number.isFinite(sec) || sec <= 0) {
|
|
9250
|
+
console.error(`--timeout 非法:${raw}(应为正数秒,如 180)`);
|
|
9251
|
+
process.exit(1);
|
|
9252
|
+
}
|
|
9253
|
+
return sec * 1e3;
|
|
9254
|
+
}
|
|
9255
|
+
function windowDays(args) {
|
|
9256
|
+
const t = opt(args, "-t", "--time");
|
|
9257
|
+
if (t) {
|
|
9258
|
+
const m = /^(\d+)([mhdw])$/.exec(t);
|
|
9259
|
+
if (!m) return 0;
|
|
9260
|
+
const n = Number(m[1]);
|
|
9261
|
+
const unit = m[2];
|
|
9262
|
+
if (unit === "w") return n * 7;
|
|
9263
|
+
if (unit === "d") return n;
|
|
9264
|
+
if (unit === "h") return n / 24;
|
|
9265
|
+
return n / 1440;
|
|
9266
|
+
}
|
|
9267
|
+
const st = opt(args, "--start-time");
|
|
9268
|
+
const et = opt(args, "--end-time");
|
|
9269
|
+
if (st && et) {
|
|
9270
|
+
const ms = Date.parse(et.replace(" ", "T")) - Date.parse(st.replace(" ", "T"));
|
|
9271
|
+
return Number.isFinite(ms) ? ms / 864e5 : 0;
|
|
9272
|
+
}
|
|
9273
|
+
return 0;
|
|
9274
|
+
}
|
|
9275
|
+
function warnWideQuery(label, args, pageSize, narrowFlags) {
|
|
9276
|
+
if (narrowFlags.some((f) => opt(args, f))) return;
|
|
9277
|
+
const days = windowDays(args);
|
|
9278
|
+
if (days < 1 && pageSize <= 200) return;
|
|
9279
|
+
const win = days ? `窗口 ${days.toFixed(1)} 天` : "窗口未识别";
|
|
9280
|
+
console.error(`⚠️ ${label} ${win} / pageSize ${pageSize} 且未限定服务:上游模糊匹配 + 全服务扫描,易触发 120s 超时;建议加 --app <appCode> 或缩短 -t(≤6h)`);
|
|
9281
|
+
}
|
|
9147
9282
|
function requireToken(api) {
|
|
9148
9283
|
if (!api.token) {
|
|
9149
9284
|
console.error("未认证。先运行:bfi auth login --token <sk-token>(或传 --token)");
|
|
@@ -9354,7 +9489,12 @@ async function runYmc(args, globalOpts) {
|
|
|
9354
9489
|
const v = opt(rest, name);
|
|
9355
9490
|
if (v) body[k] = v;
|
|
9356
9491
|
}
|
|
9357
|
-
|
|
9492
|
+
warnWideQuery("慢 SQL", rest, Number(body.pageSize), [
|
|
9493
|
+
"--app",
|
|
9494
|
+
"--trace-id",
|
|
9495
|
+
"--md5"
|
|
9496
|
+
]);
|
|
9497
|
+
emit(await api.call("ymc/warningSql", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
|
|
9358
9498
|
const d = data;
|
|
9359
9499
|
const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
|
|
9360
9500
|
const n = rows.length;
|
|
@@ -9388,7 +9528,12 @@ async function runYmc(args, globalOpts) {
|
|
|
9388
9528
|
const cost = opt(rest, "--min-cost");
|
|
9389
9529
|
if (cost) body.costTime = [Number(cost), 999999];
|
|
9390
9530
|
if (flag(rest, "--profiler")) console.error("⚠️ --profiler 是上游采样过滤器,实测常返回空集——常规取证请去掉(CPU 热点走 dump-list)");
|
|
9391
|
-
|
|
9531
|
+
warnWideQuery("慢调用", rest, Number(body.pageSize), [
|
|
9532
|
+
"--app",
|
|
9533
|
+
"--trace-id",
|
|
9534
|
+
"--method"
|
|
9535
|
+
]);
|
|
9536
|
+
emit(await api.call("ymc/warningCall", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
|
|
9392
9537
|
const d = data;
|
|
9393
9538
|
const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
|
|
9394
9539
|
const n = rows.length;
|
|
@@ -9753,27 +9898,33 @@ const HELP = {
|
|
|
9753
9898
|
⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号
|
|
9754
9899
|
("java.lang.OutOfMemoryError" 命中;简写 "OutOfMemoryError" 恒 0 命中且不报错)——sls-discipline §3
|
|
9755
9900
|
--app <appCode> 裸微服务名,前置 applicationname 限定(不限服务的检索会命中不相干服务)`,
|
|
9756
|
-
slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--json]
|
|
9901
|
+
slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--timeout <秒>] [--json]
|
|
9757
9902
|
慢 SQL 告警明细(耗时降序;摘要含 服务/耗时/行数/SQL 前段/指纹/调用栈首帧)。
|
|
9758
9903
|
参数:
|
|
9759
9904
|
-t <time> 时间窗(必填语义,服务端强校验)
|
|
9760
|
-
--sql-str 按 SQL
|
|
9761
|
-
--md5 按 SQL MD5 指纹精确过滤(同类 SQL
|
|
9905
|
+
--sql-str 按 SQL 片段过滤(表名/语句片段;模糊匹配,开销大,见下方警示)
|
|
9906
|
+
--md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类;有指纹时优先用,毫秒级)
|
|
9762
9907
|
--trace-id 按链路过滤。⚠️ 按 trace 查询时不带 --app(traceId 已唯一限定,
|
|
9763
9908
|
--app 会截断其他服务的告警行——慢 SQL 归属哪个服务事先未知)
|
|
9764
9909
|
--app 服务维度过滤(无 traceId 时必带,裸 appCode)
|
|
9910
|
+
--page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
|
|
9911
|
+
--timeout <秒> 客户端超时(默认 150s;服务端上游超时 120s,留余量先拿服务端消息)
|
|
9765
9912
|
--no-profiler 显式关闭 enableProfiler(默认自动带 'true'——不带该字段上游 500,
|
|
9766
|
-
带了才返回 sqlStack
|
|
9913
|
+
带了才返回 sqlStack 调用栈;不需要调用栈时关闭约省 40% 耗时)
|
|
9914
|
+
⚠️ --sql-str 是模糊匹配:大窗口(≥1d)必须配 --app,否则上游全服务扫描极易 120s 超时
|
|
9915
|
+
(实测 10d 无 --app 必超时;同条件加 --app 后 ~105s 成功)。窗口优先 ≤6h。
|
|
9767
9916
|
示例:
|
|
9768
9917
|
bfi ymc slowsql -t 1d --app yonbip-scm-scmmp -e core3 # 服务维度(SQL 画像)
|
|
9769
9918
|
bfi ymc slowsql -t 1d --trace-id abc -e core3 # 链路维度(勿带 --app)`,
|
|
9770
|
-
slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--json]
|
|
9919
|
+
slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--timeout <秒>] [--json]
|
|
9771
9920
|
慢调用告警明细(耗时降序;摘要含 服务/耗时/sql/rpc/redis 分解/方法/pod)。
|
|
9772
9921
|
参数:
|
|
9773
9922
|
--trace-id 按链路过滤。⚠️ 不带 --app(同 slowsql,--app 会截断下游服务告警行)
|
|
9774
9923
|
--app 服务维度过滤(裸 appCode)
|
|
9775
9924
|
--method 按调用方法过滤
|
|
9776
9925
|
--min-cost <ms> 最小耗时过滤(body.costTime=[ms, 999999])
|
|
9926
|
+
--page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
|
|
9927
|
+
--timeout <秒> 客户端超时(默认 150s)
|
|
9777
9928
|
--profiler 透传上游 enableProfiler(仅返回有 CPU 采样记录的调用);
|
|
9778
9929
|
该过滤器常返回空集,常规取证勿带,CPU 热点走 dump-list。
|
|
9779
9930
|
示例:
|
|
@@ -10655,7 +10806,7 @@ ${BOLD}bfi — 用友大业财BFI统一命令行工具${RESET}
|
|
|
10655
10806
|
${BOLD}Usage:${RESET} bfi <command> [options]
|
|
10656
10807
|
|
|
10657
10808
|
${BOLD}━━━ 认证 ━━━${RESET}
|
|
10658
|
-
auth login [--token <token>] 登录 SkillHub
|
|
10809
|
+
auth login [--token <token>] 登录 SkillHub(默认设备授权:浏览器打开并输入代码;--no-device 走隐藏输入)
|
|
10659
10810
|
auth logout 登出
|
|
10660
10811
|
auth whoami 查看当前用户
|
|
10661
10812
|
|