@p-moon/yue-cli 0.1.6
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 +91 -0
- package/bin/yue.mjs +6 -0
- package/dist/bin/yue-cli.js +219 -0
- package/dist/bin/yue.js +599 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# yue
|
|
2
|
+
|
|
3
|
+
CLI tool for [mydb.jdfmgt.com](https://mydb.jdfmgt.com/) and [Digger](https://joywatch.jd.com/digger/techApp/overview) — query databases, execute SQL, and search application logs via opencli's browser session.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
npm link
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### List data sources
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yue mydb datasources
|
|
19
|
+
yue mydb datasources --filter trade
|
|
20
|
+
yue mydb datasources --output json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Execute SQL
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# By data source name (fuzzy match)
|
|
27
|
+
yue mydb query --dbName jdinsure_trade_cds --sql 'select * from ext_orders limit 10'
|
|
28
|
+
|
|
29
|
+
# By data source ID
|
|
30
|
+
yue mydb query --dbId 25975 --sql 'select 1'
|
|
31
|
+
|
|
32
|
+
# Interactive selection (omit both --dbId and --dbName)
|
|
33
|
+
yue mydb query --sql 'select 1'
|
|
34
|
+
|
|
35
|
+
# JSON output
|
|
36
|
+
yue mydb query --dbName jdinsure_trade_cds --sql 'select 1' --output json
|
|
37
|
+
|
|
38
|
+
# Pagination
|
|
39
|
+
yue mydb query --dbName jdinsure_trade_cds --sql 'select * from big_table' --page 2 --limit 50
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Search Digger logs
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# List available apps (no --keyWord)
|
|
46
|
+
yue digger history --appName baoxian
|
|
47
|
+
|
|
48
|
+
# Search logs with keyword match (default)
|
|
49
|
+
yue digger history --appName baoxian.mall.trade --keyWord getUserAuthInfo
|
|
50
|
+
|
|
51
|
+
# Search logs with regex match
|
|
52
|
+
yue digger history --appName baoxian.mall.trade --keyWord "getUserAuthInfo.*req" --searchType regular
|
|
53
|
+
|
|
54
|
+
# With time range
|
|
55
|
+
yue digger history --appName baoxian.mall.trade --keyWord getUserAuthInfo --startTime "2026-07-01 09:00:00" --endTime "2026-07-01 18:00:00"
|
|
56
|
+
|
|
57
|
+
# Limit results
|
|
58
|
+
yue digger history --appName baoxian.mall.trade --keyWord getUserAuthInfo --limit 20
|
|
59
|
+
|
|
60
|
+
# Filter by log level
|
|
61
|
+
yue digger history --appName baoxian.mall.trade --keyWord getUserAuthInfo --logLevel ERROR
|
|
62
|
+
|
|
63
|
+
# JSON output
|
|
64
|
+
yue digger history --appName baoxian.mall.trade --keyWord getUserAuthInfo --output json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Digger options
|
|
68
|
+
|
|
69
|
+
| Flag | Description | Default |
|
|
70
|
+
|------|-------------|---------|
|
|
71
|
+
| `--appName <prefix>` | App name or prefix for fuzzy match | — |
|
|
72
|
+
| `--keyWord <keyword>` | Keyword or regex to search in logs | — |
|
|
73
|
+
| `--searchType <type>` | `exact` (keyword match) or `regular` (regex) | `exact` |
|
|
74
|
+
| `--startTime <datetime>` | Start time, e.g. `"2026-07-01 09:00:00"` | 1 hour ago |
|
|
75
|
+
| `--endTime <datetime>` | End time, e.g. `"2026-07-01 18:00:00"` | now |
|
|
76
|
+
| `--limit <n>` | Max log entries to return | 50 |
|
|
77
|
+
| `--logLevel <level>` | Log level filter | `ALL` |
|
|
78
|
+
| `--output <fmt>` | `plain` (default) or `json` | `plain` |
|
|
79
|
+
|
|
80
|
+
> **Note:** `--appName` accepts both dot notation (`baoxian.mall.trade`) and hyphen notation (`baoxian-mall-trade`). Without `--keyWord`, it lists matching apps with usage hints.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Run directly (no build)
|
|
86
|
+
npm run dev -- mydb query --dbName jdinsure_trade_cds --sql 'select 1'
|
|
87
|
+
npm run dev -- digger history --appName baoxian --keyWord getUserAuthInfo
|
|
88
|
+
|
|
89
|
+
# Build
|
|
90
|
+
npm run build
|
|
91
|
+
```
|
package/bin/yue.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const __main = new URL('file:///' + join(__dirname, '..', 'dist', 'bin', 'yue.js').replace(/^\//, ''));
|
|
6
|
+
await import(__main.href);
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/datasources.ts
|
|
7
|
+
import Table from "cli-table3";
|
|
8
|
+
|
|
9
|
+
// src/lib/browser.ts
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
var opencliMainUrl = await import.meta.resolve("@jackwener/opencli");
|
|
14
|
+
var opencliMainPath = new URL(opencliMainUrl).pathname;
|
|
15
|
+
var opencliSrcDir = path.dirname(opencliMainPath);
|
|
16
|
+
var { BrowserBridge } = await import(pathToFileURL(path.join(opencliSrcDir, "browser", "index.js")).href);
|
|
17
|
+
var { DEFAULT_BROWSER_CONNECT_TIMEOUT } = await import(pathToFileURL(path.join(opencliSrcDir, "runtime.js")).href);
|
|
18
|
+
var SITE = "mydb";
|
|
19
|
+
async function withBrowser(fn) {
|
|
20
|
+
const browser = new BrowserBridge();
|
|
21
|
+
try {
|
|
22
|
+
const page = await browser.connect({
|
|
23
|
+
timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
|
|
24
|
+
session: `site:${SITE}:${crypto.randomUUID()}`,
|
|
25
|
+
windowMode: "background"
|
|
26
|
+
});
|
|
27
|
+
await page.goto("https://mydb.jdfmgt.com/");
|
|
28
|
+
return await fn(page);
|
|
29
|
+
} finally {
|
|
30
|
+
await browser.close().catch(() => {
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/lib/mydb-api.ts
|
|
36
|
+
var BASE_URL = "https://mydb.jdfmgt.com";
|
|
37
|
+
async function fetchDatasources(page, filter) {
|
|
38
|
+
const q = filter ? `&query=${encodeURIComponent(filter)}` : "";
|
|
39
|
+
const url = `${BASE_URL}/dataSourceList?${q}&page=1&start=0&limit=200&_dc=${Date.now()}`;
|
|
40
|
+
const json = await page.evaluate(async (fetchUrl) => {
|
|
41
|
+
const resp = await fetch(fetchUrl, { credentials: "include" });
|
|
42
|
+
return resp.json();
|
|
43
|
+
}, url);
|
|
44
|
+
const rows = json.data ?? json.rows ?? json.list ?? [];
|
|
45
|
+
return rows.map((r) => ({
|
|
46
|
+
id: r.dbId ?? r.id ?? r.DB_ID,
|
|
47
|
+
name: r.dbName ?? r.name ?? r.DB_NAME ?? r.text
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function resolveDbIdByName(sources, name) {
|
|
51
|
+
const lower = name.toLowerCase();
|
|
52
|
+
let match = sources.find((s) => s.name === name);
|
|
53
|
+
if (!match) {
|
|
54
|
+
match = sources.find((s) => {
|
|
55
|
+
const base = s.name.split("(")[0].trim().toLowerCase();
|
|
56
|
+
return base === lower;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!match) {
|
|
60
|
+
const candidates = sources.filter((s) => s.name.toLowerCase().includes(lower));
|
|
61
|
+
if (candidates.length > 0) {
|
|
62
|
+
candidates.sort((a, b) => a.name.length - b.name.length);
|
|
63
|
+
match = candidates[0];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return match;
|
|
67
|
+
}
|
|
68
|
+
async function executeSql(page, sql, dbId, pageNum = 1, limit = 25) {
|
|
69
|
+
const start = (pageNum - 1) * limit;
|
|
70
|
+
const url = `${BASE_URL}/commitSql?resultSetIndex=0&_dc=${Date.now()}`;
|
|
71
|
+
const body = `sqlText=${encodeURIComponent(sql)}&dbId=${dbId}&page=${pageNum}&start=${start}&limit=${limit}`;
|
|
72
|
+
const rawText = await page.evaluate(async (args) => {
|
|
73
|
+
const resp = await fetch(args.url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
77
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
78
|
+
},
|
|
79
|
+
credentials: "include",
|
|
80
|
+
body: args.body
|
|
81
|
+
});
|
|
82
|
+
return resp.text();
|
|
83
|
+
}, { url, body });
|
|
84
|
+
const json = JSON.parse(rawText);
|
|
85
|
+
if (json.errorCode !== 0 && json.errorCode !== void 0) {
|
|
86
|
+
throw new Error(`mydb query failed: ${json.errMsg || json.msg || JSON.stringify(json)}`);
|
|
87
|
+
}
|
|
88
|
+
const resultInfo = json.resultInfo ?? json.data ?? {};
|
|
89
|
+
const rows = resultInfo.data ?? (Array.isArray(json.data) ? json.data : []);
|
|
90
|
+
return {
|
|
91
|
+
rows,
|
|
92
|
+
tip: resultInfo.tip ?? "",
|
|
93
|
+
duration: resultInfo.duration ?? 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/commands/datasources.ts
|
|
98
|
+
function registerDatasourcesCommand(program) {
|
|
99
|
+
program.command("datasources").description("List available data sources on mydb").option("--filter <keyword>", "Filter data sources by keyword").option("--output <format>", "Output format: table (default) or json", "table").action(async (opts) => {
|
|
100
|
+
await withBrowser(async (page) => {
|
|
101
|
+
const sources = await fetchDatasources(page, opts.filter);
|
|
102
|
+
if (!sources || sources.length === 0) {
|
|
103
|
+
console.log("No data sources found.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if ((opts.output ?? "table").toLowerCase() === "json") {
|
|
107
|
+
console.log(JSON.stringify(sources, null, 2));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const table = new Table({
|
|
111
|
+
head: ["id", "name"],
|
|
112
|
+
style: { head: ["cyan"], border: ["gray"] },
|
|
113
|
+
wordWrap: true
|
|
114
|
+
});
|
|
115
|
+
for (const s of sources) {
|
|
116
|
+
table.push([s.id, s.name]);
|
|
117
|
+
}
|
|
118
|
+
console.log(table.toString());
|
|
119
|
+
console.log(`
|
|
120
|
+
${sources.length} data source(s)`);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/commands/query.ts
|
|
126
|
+
import Table2 from "cli-table3";
|
|
127
|
+
function registerQueryCommand(program) {
|
|
128
|
+
program.command("query").description("Execute a SQL query on mydb").requiredOption("--sql <sql>", "SQL statement to execute").option("--dbId <id>", "Data source ID", parseInt).option("--dbName <name>", "Data source name (fuzzy match, alternative to --dbId)").option("--page <n>", "Page number (default: 1)", parseInt, 1).option("--limit <n>", "Rows per page (default: 25)", parseInt, 25).option("--output <format>", "Output format: table (default) or json", "table").action(async (opts) => {
|
|
129
|
+
const sql = opts.sql;
|
|
130
|
+
let dbId = opts.dbId;
|
|
131
|
+
const dbName = opts.dbName;
|
|
132
|
+
const pageNum = opts.page ?? 1;
|
|
133
|
+
const limitNum = opts.limit ?? 25;
|
|
134
|
+
const format = (opts.output ?? "table").toLowerCase();
|
|
135
|
+
if (!sql?.trim()) {
|
|
136
|
+
console.error("Error: --sql is required");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
await withBrowser(async (page) => {
|
|
140
|
+
if (!dbId && dbName) {
|
|
141
|
+
const sources = await fetchDatasources(page);
|
|
142
|
+
const match = resolveDbIdByName(sources, dbName);
|
|
143
|
+
if (!match) {
|
|
144
|
+
console.error(`Error: Data source "${dbName}" not found.`);
|
|
145
|
+
console.error('Run "yue-cli mydb datasources" to list available sources.');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
dbId = match.id;
|
|
149
|
+
}
|
|
150
|
+
if (!dbId) {
|
|
151
|
+
const sources = await fetchDatasources(page);
|
|
152
|
+
if (sources.length === 0) {
|
|
153
|
+
console.error("No data sources available. Make sure you are logged in.");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const listText = sources.map((s, i) => ` ${i + 1}. [${s.id}] ${s.name}`).join("\n");
|
|
157
|
+
const answer = await page.evaluate((msg) => {
|
|
158
|
+
return window.prompt(msg, "");
|
|
159
|
+
}, `Select a data source (enter number, dbId, or name):
|
|
160
|
+
${listText}`);
|
|
161
|
+
if (!answer) {
|
|
162
|
+
console.error("No data source selected.");
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const trimmed = String(answer).trim();
|
|
166
|
+
const num = Number(trimmed);
|
|
167
|
+
if (!isNaN(num) && num >= 1 && num <= sources.length) {
|
|
168
|
+
dbId = sources[num - 1].id;
|
|
169
|
+
} else if (!isNaN(num)) {
|
|
170
|
+
dbId = num;
|
|
171
|
+
} else {
|
|
172
|
+
const match = resolveDbIdByName(sources, trimmed);
|
|
173
|
+
if (!match) {
|
|
174
|
+
console.error(`No data source matching "${trimmed}".`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
dbId = match.id;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const result = await executeSql(page, sql, dbId, pageNum, limitNum);
|
|
181
|
+
if (!result.rows || result.rows.length === 0) {
|
|
182
|
+
console.log(result.tip || "No rows returned");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (format === "json") {
|
|
186
|
+
console.log(JSON.stringify(result.rows, null, 2));
|
|
187
|
+
if (result.tip) console.error(result.tip);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const hiddenPrefixes = ["cdsColumn"];
|
|
191
|
+
const allCols = Object.keys(result.rows[0]);
|
|
192
|
+
const cols = allCols.filter((c) => !hiddenPrefixes.some((p) => c.startsWith(p)));
|
|
193
|
+
const table = new Table2({
|
|
194
|
+
head: cols,
|
|
195
|
+
style: { head: ["cyan"], border: ["gray"] },
|
|
196
|
+
wordWrap: true
|
|
197
|
+
});
|
|
198
|
+
for (const row of result.rows) {
|
|
199
|
+
table.push(cols.map((c) => String(row[c] ?? "")));
|
|
200
|
+
}
|
|
201
|
+
console.log(table.toString());
|
|
202
|
+
if (result.tip) console.log(`
|
|
203
|
+
${result.tip}`);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/cli.ts
|
|
209
|
+
function run() {
|
|
210
|
+
const program = new Command();
|
|
211
|
+
program.name("yue-cli").description("CLI tool for mydb.jdfmgt.com \u2014 query databases and execute SQL").version("0.1.0");
|
|
212
|
+
const mydb = program.command("mydb").description("mydb.jdfmgt.com operations");
|
|
213
|
+
registerDatasourcesCommand(mydb);
|
|
214
|
+
registerQueryCommand(mydb);
|
|
215
|
+
program.parse();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// bin/yue-cli.ts
|
|
219
|
+
run();
|
package/dist/bin/yue.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/datasources.ts
|
|
7
|
+
import Table from "cli-table3";
|
|
8
|
+
|
|
9
|
+
// src/lib/browser.ts
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as crypto from "node:crypto";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
var opencliMainUrl = await import.meta.resolve("@jackwener/opencli");
|
|
17
|
+
var opencliMainPath = new URL(opencliMainUrl).pathname;
|
|
18
|
+
var opencliSrcDir = path.dirname(opencliMainPath);
|
|
19
|
+
var { BrowserBridge } = await import(pathToFileURL(path.join(opencliSrcDir, "browser", "index.js")).href);
|
|
20
|
+
var { DEFAULT_BROWSER_CONNECT_TIMEOUT } = await import(pathToFileURL(path.join(opencliSrcDir, "runtime.js")).href);
|
|
21
|
+
var SITE = "mydb";
|
|
22
|
+
var CHROME_WEBSTORE_URL = "https://joyspace.jd.com/pages/OQFM9MdxR4PEgOJrnmQk";
|
|
23
|
+
function findChromePath() {
|
|
24
|
+
const candidates = [];
|
|
25
|
+
if (os.platform() === "darwin") {
|
|
26
|
+
candidates.push(
|
|
27
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
28
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
29
|
+
);
|
|
30
|
+
} else if (os.platform() === "linux") {
|
|
31
|
+
candidates.push(
|
|
32
|
+
"/usr/bin/google-chrome",
|
|
33
|
+
"/usr/bin/google-chrome-stable",
|
|
34
|
+
"/usr/bin/chromium",
|
|
35
|
+
"/usr/bin/chromium-browser"
|
|
36
|
+
);
|
|
37
|
+
} else if (os.platform() === "win32") {
|
|
38
|
+
const pf = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
39
|
+
const pfx86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
40
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? "";
|
|
41
|
+
candidates.push(
|
|
42
|
+
`${pf}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
43
|
+
`${pfx86}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
44
|
+
`${localAppData}\\Google\\Chrome\\Application\\chrome.exe`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
if (fs.existsSync(p)) return p;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const cmd = os.platform() === "win32" ? "where chrome" : "which google-chrome || which chromium";
|
|
52
|
+
const result = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
53
|
+
if (result && fs.existsSync(result.split("\n")[0])) return result.split("\n")[0];
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function openExtensionInstallPage() {
|
|
59
|
+
const platform2 = os.platform();
|
|
60
|
+
try {
|
|
61
|
+
if (platform2 === "darwin") {
|
|
62
|
+
if (findChromePath()) {
|
|
63
|
+
execSync(`open -a "Google Chrome" "${CHROME_WEBSTORE_URL}"`, { stdio: "ignore" });
|
|
64
|
+
} else {
|
|
65
|
+
execSync(`open "${CHROME_WEBSTORE_URL}"`, { stdio: "ignore" });
|
|
66
|
+
}
|
|
67
|
+
} else if (platform2 === "linux") {
|
|
68
|
+
execSync(`xdg-open "${CHROME_WEBSTORE_URL}"`, { stdio: "ignore" });
|
|
69
|
+
} else if (platform2 === "win32") {
|
|
70
|
+
execSync(`start "${CHROME_WEBSTORE_URL}"`, { stdio: "ignore", shell: "cmd.exe" });
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function getDaemonHealth() {
|
|
76
|
+
const mod = await import(pathToFileURL(path.join(opencliSrcDir, "browser", "daemon-transport.js")).href);
|
|
77
|
+
return mod.getDaemonHealth();
|
|
78
|
+
}
|
|
79
|
+
async function checkExtensionStatus() {
|
|
80
|
+
try {
|
|
81
|
+
const health = await getDaemonHealth();
|
|
82
|
+
return {
|
|
83
|
+
state: health.state,
|
|
84
|
+
extensionMissing: health.state === "no-extension",
|
|
85
|
+
daemonStopped: health.state === "stopped"
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return { state: "unknown", extensionMissing: false, daemonStopped: false };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function guideExtensionInstall() {
|
|
92
|
+
const chromePath = findChromePath();
|
|
93
|
+
if (!chromePath) {
|
|
94
|
+
console.error(
|
|
95
|
+
"\n\u274C Chrome/Chromium \u672A\u68C0\u6D4B\u5230\u3002\n \u8BF7\u5148\u5B89\u88C5 Google Chrome: https://www.google.com/chrome/\n"
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
console.error(
|
|
100
|
+
"\n\u26A0\uFE0F YueCli Browser Bridge \u6269\u5C55\u672A\u8FDE\u63A5\u3002\n yue \u9700\u8981\u6B64\u6269\u5C55\u624D\u80FD\u4E0E\u6D4F\u89C8\u5668\u4EA4\u4E92\u3002\n"
|
|
101
|
+
);
|
|
102
|
+
console.error("\u{1F4E6} \u6B63\u5728\u6253\u5F00 Chrome Web Store \u5B89\u88C5 YueCLI \u6269\u5C55...");
|
|
103
|
+
openExtensionInstallPage();
|
|
104
|
+
console.error(
|
|
105
|
+
`
|
|
106
|
+
\u5B89\u88C5\u6269\u5C55\u540E\uFF1A
|
|
107
|
+
1. \u786E\u4FDD\u6269\u5C55\u5728 chrome://extensions \u4E2D\u5DF2\u542F\u7528
|
|
108
|
+
2. Chrome \u5DE5\u5177\u680F\u5E94\u51FA\u73B0 YueCli \u56FE\u6807
|
|
109
|
+
3. \u91CD\u65B0\u8FD0\u884C yue \u547D\u4EE4
|
|
110
|
+
|
|
111
|
+
\u624B\u52A8\u5B89\u88C5\u5730\u5740: ${CHROME_WEBSTORE_URL}
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
async function withBrowser(fn, opts) {
|
|
117
|
+
const preCheck = await checkExtensionStatus();
|
|
118
|
+
if (preCheck.extensionMissing) {
|
|
119
|
+
guideExtensionInstall();
|
|
120
|
+
}
|
|
121
|
+
const browser = new BrowserBridge();
|
|
122
|
+
try {
|
|
123
|
+
const page = await browser.connect({
|
|
124
|
+
timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
|
|
125
|
+
session: `site:${SITE}:${crypto.randomUUID()}`,
|
|
126
|
+
windowMode: "background"
|
|
127
|
+
});
|
|
128
|
+
const url = opts?.targetUrl ?? "https://mydb.jdfmgt.com/";
|
|
129
|
+
await page.goto(url);
|
|
130
|
+
return await fn(page);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const errorCode = err?.kind ?? err?.code;
|
|
133
|
+
if (errorCode === "extension-not-connected" || errorCode === "extension_not_connected") {
|
|
134
|
+
guideExtensionInstall();
|
|
135
|
+
}
|
|
136
|
+
if (err?.name === "BrowserConnectError" || err?.code === "BROWSER_CONNECT") {
|
|
137
|
+
const postCheck = await checkExtensionStatus();
|
|
138
|
+
if (postCheck.extensionMissing) {
|
|
139
|
+
guideExtensionInstall();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
await browser.close().catch(() => {
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/lib/mydb-api.ts
|
|
150
|
+
var BASE_URL = "https://mydb.jdfmgt.com";
|
|
151
|
+
async function fetchDatasources(page, filter) {
|
|
152
|
+
const q = filter ? `&query=${encodeURIComponent(filter)}` : "";
|
|
153
|
+
const url = `${BASE_URL}/dataSourceList?${q}&page=1&start=0&limit=200&_dc=${Date.now()}`;
|
|
154
|
+
const json = await page.evaluate(async (fetchUrl) => {
|
|
155
|
+
const resp = await fetch(fetchUrl, { credentials: "include" });
|
|
156
|
+
return resp.json();
|
|
157
|
+
}, url);
|
|
158
|
+
const rows = json.data ?? json.rows ?? json.list ?? [];
|
|
159
|
+
return rows.map((r) => ({
|
|
160
|
+
id: r.dbId ?? r.id ?? r.DB_ID,
|
|
161
|
+
name: r.dbName ?? r.name ?? r.DB_NAME ?? r.text
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
function resolveDbIdByName(sources, name) {
|
|
165
|
+
const lower = name.toLowerCase();
|
|
166
|
+
let match = sources.find((s) => s.name === name);
|
|
167
|
+
if (!match) {
|
|
168
|
+
match = sources.find((s) => {
|
|
169
|
+
const base = s.name.split("(")[0].trim().toLowerCase();
|
|
170
|
+
return base === lower;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (!match) {
|
|
174
|
+
const candidates = sources.filter((s) => s.name.toLowerCase().includes(lower));
|
|
175
|
+
if (candidates.length > 0) {
|
|
176
|
+
candidates.sort((a, b) => a.name.length - b.name.length);
|
|
177
|
+
match = candidates[0];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return match;
|
|
181
|
+
}
|
|
182
|
+
async function executeSql(page, sql, dbId, pageNum = 1, limit = 25) {
|
|
183
|
+
const start = (pageNum - 1) * limit;
|
|
184
|
+
const url = `${BASE_URL}/commitSql?resultSetIndex=0&_dc=${Date.now()}`;
|
|
185
|
+
const body = `sqlText=${encodeURIComponent(sql)}&dbId=${dbId}&page=${pageNum}&start=${start}&limit=${limit}`;
|
|
186
|
+
const rawText = await page.evaluate(async (args) => {
|
|
187
|
+
const resp = await fetch(args.url, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
191
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
192
|
+
},
|
|
193
|
+
credentials: "include",
|
|
194
|
+
body: args.body
|
|
195
|
+
});
|
|
196
|
+
return resp.text();
|
|
197
|
+
}, { url, body });
|
|
198
|
+
const json = JSON.parse(rawText);
|
|
199
|
+
if (json.errorCode !== 0 && json.errorCode !== void 0) {
|
|
200
|
+
throw new Error(`mydb query failed: ${json.errMsg || json.msg || JSON.stringify(json)}`);
|
|
201
|
+
}
|
|
202
|
+
const resultInfo = json.resultInfo ?? json.data ?? {};
|
|
203
|
+
const rows = resultInfo.data ?? (Array.isArray(json.data) ? json.data : []);
|
|
204
|
+
return {
|
|
205
|
+
rows,
|
|
206
|
+
tip: resultInfo.tip ?? "",
|
|
207
|
+
duration: resultInfo.duration ?? 0
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/commands/datasources.ts
|
|
212
|
+
function registerDatasourcesCommand(program) {
|
|
213
|
+
program.command("datasources").description("List available data sources on mydb").option("--filter <keyword>", "Filter data sources by keyword").option("--output <format>", "Output format: table (default) or json", "table").action(async (opts) => {
|
|
214
|
+
await withBrowser(async (page) => {
|
|
215
|
+
const sources = await fetchDatasources(page, opts.filter);
|
|
216
|
+
if (!sources || sources.length === 0) {
|
|
217
|
+
console.log("No data sources found.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if ((opts.output ?? "table").toLowerCase() === "json") {
|
|
221
|
+
console.log(JSON.stringify(sources, null, 2));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const table = new Table({
|
|
225
|
+
head: ["id", "name"],
|
|
226
|
+
style: { head: ["cyan"], border: ["gray"] },
|
|
227
|
+
wordWrap: true
|
|
228
|
+
});
|
|
229
|
+
for (const s of sources) {
|
|
230
|
+
table.push([s.id, s.name]);
|
|
231
|
+
}
|
|
232
|
+
console.log(table.toString());
|
|
233
|
+
console.log(`
|
|
234
|
+
${sources.length} data source(s)`);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/commands/query.ts
|
|
240
|
+
import Table2 from "cli-table3";
|
|
241
|
+
function registerQueryCommand(program) {
|
|
242
|
+
program.command("query").description("Execute a SQL query on mydb").requiredOption("--sql <sql>", "SQL statement to execute").option("--dbId <id>", "Data source ID", parseInt).option("--dbName <name>", "Data source name (fuzzy match, alternative to --dbId)").option("--page <n>", "Page number (default: 1)", parseInt, 1).option("--limit <n>", "Rows per page (default: 25)", parseInt, 25).option("--output <format>", "Output format: table (default) or json", "table").action(async (opts) => {
|
|
243
|
+
const sql = opts.sql;
|
|
244
|
+
let dbId = opts.dbId;
|
|
245
|
+
const dbName = opts.dbName;
|
|
246
|
+
const pageNum = opts.page ?? 1;
|
|
247
|
+
const limitNum = opts.limit ?? 25;
|
|
248
|
+
const format = (opts.output ?? "table").toLowerCase();
|
|
249
|
+
if (!sql?.trim()) {
|
|
250
|
+
console.error("Error: --sql is required");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
await withBrowser(async (page) => {
|
|
254
|
+
if (!dbId && dbName) {
|
|
255
|
+
const sources = await fetchDatasources(page);
|
|
256
|
+
const match = resolveDbIdByName(sources, dbName);
|
|
257
|
+
if (!match) {
|
|
258
|
+
console.error(`Error: Data source "${dbName}" not found.`);
|
|
259
|
+
console.error('Run "yue mydb datasources" to list available sources.');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
dbId = match.id;
|
|
263
|
+
}
|
|
264
|
+
if (!dbId) {
|
|
265
|
+
const sources = await fetchDatasources(page);
|
|
266
|
+
if (sources.length === 0) {
|
|
267
|
+
console.error("No data sources available. Make sure you are logged in.");
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const listText = sources.map((s, i) => ` ${i + 1}. [${s.id}] ${s.name}`).join("\n");
|
|
271
|
+
const answer = await page.evaluate((msg) => {
|
|
272
|
+
return window.prompt(msg, "");
|
|
273
|
+
}, `Select a data source (enter number, dbId, or name):
|
|
274
|
+
${listText}`);
|
|
275
|
+
if (!answer) {
|
|
276
|
+
console.error("No data source selected.");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
const trimmed = String(answer).trim();
|
|
280
|
+
const num = Number(trimmed);
|
|
281
|
+
if (!isNaN(num) && num >= 1 && num <= sources.length) {
|
|
282
|
+
dbId = sources[num - 1].id;
|
|
283
|
+
} else if (!isNaN(num)) {
|
|
284
|
+
dbId = num;
|
|
285
|
+
} else {
|
|
286
|
+
const match = resolveDbIdByName(sources, trimmed);
|
|
287
|
+
if (!match) {
|
|
288
|
+
console.error(`No data source matching "${trimmed}".`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
dbId = match.id;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const result = await executeSql(page, sql, dbId, pageNum, limitNum);
|
|
295
|
+
if (!result.rows || result.rows.length === 0) {
|
|
296
|
+
console.log(result.tip || "No rows returned");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (format === "json") {
|
|
300
|
+
console.log(JSON.stringify(result.rows, null, 2));
|
|
301
|
+
if (result.tip) console.error(result.tip);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const hiddenPrefixes = ["cdsColumn"];
|
|
305
|
+
const allCols = Object.keys(result.rows[0]);
|
|
306
|
+
const cols = allCols.filter((c) => !hiddenPrefixes.some((p) => c.startsWith(p)));
|
|
307
|
+
const table = new Table2({
|
|
308
|
+
head: cols,
|
|
309
|
+
style: { head: ["cyan"], border: ["gray"] },
|
|
310
|
+
wordWrap: true
|
|
311
|
+
});
|
|
312
|
+
for (const row of result.rows) {
|
|
313
|
+
table.push(cols.map((c) => String(row[c] ?? "")));
|
|
314
|
+
}
|
|
315
|
+
console.log(table.toString());
|
|
316
|
+
if (result.tip) console.log(`
|
|
317
|
+
${result.tip}`);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/commands/digger.ts
|
|
323
|
+
import Table3 from "cli-table3";
|
|
324
|
+
|
|
325
|
+
// src/lib/digger-api.ts
|
|
326
|
+
import * as crypto2 from "node:crypto";
|
|
327
|
+
var BASE_URL2 = "https://digger.jd.com";
|
|
328
|
+
function diggerXhr(page, method, url, body) {
|
|
329
|
+
return page.evaluate((args) => {
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
const xhr = new XMLHttpRequest();
|
|
332
|
+
xhr.open(args.method, args.url, true);
|
|
333
|
+
xhr.withCredentials = true;
|
|
334
|
+
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
|
|
335
|
+
if (args.body) {
|
|
336
|
+
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
337
|
+
}
|
|
338
|
+
xhr.onload = () => {
|
|
339
|
+
try {
|
|
340
|
+
resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, body: JSON.parse(xhr.responseText) });
|
|
341
|
+
} catch {
|
|
342
|
+
resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, body: xhr.responseText });
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
xhr.onerror = () => resolve({ ok: false, status: 0, body: "Network error" });
|
|
346
|
+
xhr.send(args.body || null);
|
|
347
|
+
});
|
|
348
|
+
}, { method, url, body: body ? JSON.stringify(body) : void 0 });
|
|
349
|
+
}
|
|
350
|
+
function normalizeAppName(name) {
|
|
351
|
+
return name.replace(/\./g, "-");
|
|
352
|
+
}
|
|
353
|
+
async function fuzzySearchApps(page, appFuzzy) {
|
|
354
|
+
const normalized = normalizeAppName(appFuzzy);
|
|
355
|
+
const url = `${BASE_URL2}/app/fuzzy/user?appFuzzy=${encodeURIComponent(normalized)}`;
|
|
356
|
+
const result = await diggerXhr(page, "GET", url);
|
|
357
|
+
if (!result.ok) {
|
|
358
|
+
throw new Error(`Digger API error: HTTP ${result.status} \u2014 ${JSON.stringify(result.body)}`);
|
|
359
|
+
}
|
|
360
|
+
const json = result.body;
|
|
361
|
+
const list = Array.isArray(json) ? json : json.data ?? json.result ?? [];
|
|
362
|
+
return list.map((r) => ({
|
|
363
|
+
appName: r.appName ?? r.app ?? r.name,
|
|
364
|
+
appDesc: r.appDesc ?? r.appDisplayName ?? r.desc ?? "",
|
|
365
|
+
level: r.level ?? "",
|
|
366
|
+
online: r.online ?? true
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
async function searchLogs(page, opts) {
|
|
370
|
+
const now = opts.endTime ?? Date.now();
|
|
371
|
+
const oneHourAgo = opts.startTime ?? now - 60 * 60 * 1e3;
|
|
372
|
+
const body = {
|
|
373
|
+
apps: [opts.appName],
|
|
374
|
+
startTime: oneHourAgo,
|
|
375
|
+
endTime: now,
|
|
376
|
+
keyword: opts.keyword,
|
|
377
|
+
exclude: "",
|
|
378
|
+
filePaths: [],
|
|
379
|
+
logLevel: opts.logLevel ?? "ALL",
|
|
380
|
+
limit: opts.limit ?? 50,
|
|
381
|
+
logSize: "",
|
|
382
|
+
station: "chinaStation",
|
|
383
|
+
searchType: opts.searchType ?? "exact",
|
|
384
|
+
productName: "app",
|
|
385
|
+
direction: "backward",
|
|
386
|
+
rentionPolicy: "LOKI",
|
|
387
|
+
dirEnabled: false,
|
|
388
|
+
showPlain: false,
|
|
389
|
+
searchId: crypto2.randomUUID(),
|
|
390
|
+
searchIndex: 0,
|
|
391
|
+
searchEndTimeStr: null,
|
|
392
|
+
coefficient: null
|
|
393
|
+
};
|
|
394
|
+
const url = `${BASE_URL2}/log/search/all`;
|
|
395
|
+
const result = await diggerXhr(page, "POST", url, body);
|
|
396
|
+
if (!result.ok) {
|
|
397
|
+
throw new Error(`Digger log search error: HTTP ${result.status} \u2014 ${JSON.stringify(result.body)}`);
|
|
398
|
+
}
|
|
399
|
+
const json = result.body;
|
|
400
|
+
const tData = json.tData ?? {};
|
|
401
|
+
const logArray = Array.isArray(tData) ? tData : tData.data ?? json.data ?? [];
|
|
402
|
+
const total = tData.total ?? json.total ?? logArray.length;
|
|
403
|
+
const entries = logArray.map((item) => ({
|
|
404
|
+
timestamp: item.timestamp ?? "",
|
|
405
|
+
level: item.level ?? "INFO",
|
|
406
|
+
message: item.m_s_g ?? item.message ?? item.msg ?? item.log ?? "",
|
|
407
|
+
host: item.host ?? "",
|
|
408
|
+
filePath: item.filePath ?? "",
|
|
409
|
+
app: item.app ?? "",
|
|
410
|
+
thread: item.t_h_r ?? "",
|
|
411
|
+
className: item.c_l_s ?? "",
|
|
412
|
+
traceId: item.traceId ?? item.pfinderTraceId ?? null
|
|
413
|
+
}));
|
|
414
|
+
return { entries, total };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/commands/digger.ts
|
|
418
|
+
var RED = "\x1B[31m";
|
|
419
|
+
var YELLOW = "\x1B[33m";
|
|
420
|
+
var GREEN = "\x1B[32m";
|
|
421
|
+
var GRAY = "\x1B[90m";
|
|
422
|
+
var CYAN = "\x1B[36m";
|
|
423
|
+
var DIM = "\x1B[2m";
|
|
424
|
+
var RESET = "\x1B[0m";
|
|
425
|
+
function formatTimestamp(ts) {
|
|
426
|
+
if (!ts) return "-";
|
|
427
|
+
const num = Number(ts);
|
|
428
|
+
if (isNaN(num)) return ts;
|
|
429
|
+
const ms = num > 1e15 ? Math.floor(num / 1e6) : num;
|
|
430
|
+
return new Date(ms).toLocaleString("zh-CN", { hour12: false });
|
|
431
|
+
}
|
|
432
|
+
function colorizeLevel(level) {
|
|
433
|
+
const upper = (level ?? "").toUpperCase();
|
|
434
|
+
switch (upper) {
|
|
435
|
+
case "ERROR":
|
|
436
|
+
case "FATAL":
|
|
437
|
+
return `${RED}${upper}${RESET}`;
|
|
438
|
+
case "WARN":
|
|
439
|
+
case "WARNING":
|
|
440
|
+
return `${YELLOW}${upper}${RESET}`;
|
|
441
|
+
case "INFO":
|
|
442
|
+
return `${GREEN}${upper}${RESET}`;
|
|
443
|
+
case "DEBUG":
|
|
444
|
+
return `${GRAY}${upper}${RESET}`;
|
|
445
|
+
default:
|
|
446
|
+
return upper || "-";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function registerDiggerCommand(program) {
|
|
450
|
+
const digger = program.command("digger").description("Digger (joywatch.jd.com) log operations");
|
|
451
|
+
digger.command("history").description("Search logs in Digger").option("--appName <prefix>", "Application name prefix for fuzzy match").option("--keyWord <keyword>", "Keyword to search in logs").option("--limit <n>", "Max log entries to return (default: 50)", parseInt, 50).option("--logLevel <level>", "Log level filter (default: ALL)", "ALL").option("--searchType <type>", "Search type: exact (keyword match, default) or regular (regex)", "exact").option("--startTime <datetime>", 'Start time (e.g. "2026-07-01 09:00:00" or "2026-07-01T09:00")').option("--endTime <datetime>", 'End time (e.g. "2026-07-01 18:00:00" or "2026-07-01T18:00")').option("--output <format>", "Output format: plain (default) or json", "plain").action(async (opts) => {
|
|
452
|
+
const appFuzzy = opts.appName;
|
|
453
|
+
const keyword = opts.keyWord;
|
|
454
|
+
const limit = opts.limit ?? 50;
|
|
455
|
+
const logLevel = opts.logLevel ?? "ALL";
|
|
456
|
+
const searchType = (opts.searchType ?? "exact").toLowerCase();
|
|
457
|
+
const format = (opts.output ?? "plain").toLowerCase();
|
|
458
|
+
if (searchType !== "exact" && searchType !== "regular") {
|
|
459
|
+
console.error(`\u274C Invalid --searchType: "${opts.searchType}". Use "exact" or "regular".`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
const startTime = opts.startTime ? new Date(opts.startTime).getTime() : void 0;
|
|
463
|
+
const endTime = opts.endTime ? new Date(opts.endTime).getTime() : void 0;
|
|
464
|
+
if (opts.startTime && isNaN(startTime)) {
|
|
465
|
+
console.error(`\u274C Invalid --startTime format: "${opts.startTime}". Use "2026-07-01 09:00:00" or "2026-07-01T09:00".`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
if (opts.endTime && isNaN(endTime)) {
|
|
469
|
+
console.error(`\u274C Invalid --endTime format: "${opts.endTime}". Use "2026-07-01 18:00:00" or "2026-07-01T18:00".`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
await withBrowser(async (page) => {
|
|
473
|
+
try {
|
|
474
|
+
await page.waitForNetworkIdle?.();
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
const searchPrefix = appFuzzy ? appFuzzy.replace(/\./g, "-").split("-")[0] : "";
|
|
478
|
+
console.log(`\u{1F50D} Searching apps${searchPrefix ? ` matching "${searchPrefix}"` : ""}...`);
|
|
479
|
+
let apps;
|
|
480
|
+
try {
|
|
481
|
+
apps = await fuzzySearchApps(page, appFuzzy || "");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.error(`\u274C Failed to search apps: ${err.message}`);
|
|
484
|
+
console.error(" Make sure you are logged in to joywatch.jd.com/digger in your browser.");
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
if (!apps || apps.length === 0) {
|
|
488
|
+
console.error(`\u274C No apps found${searchPrefix ? ` matching "${appFuzzy}"` : ""}.`);
|
|
489
|
+
console.error(" Try a different prefix or check your access.");
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
if (!keyword) {
|
|
493
|
+
const table = new Table3({
|
|
494
|
+
head: ["appName", "description"],
|
|
495
|
+
style: { head: ["cyan"], border: ["gray"] },
|
|
496
|
+
wordWrap: true,
|
|
497
|
+
colWidths: [35, 50]
|
|
498
|
+
});
|
|
499
|
+
for (const app of apps) {
|
|
500
|
+
table.push([app.appName, app.appDesc || "-"]);
|
|
501
|
+
}
|
|
502
|
+
console.log("");
|
|
503
|
+
console.log(table.toString());
|
|
504
|
+
console.log(`
|
|
505
|
+
\u{1F4CB} ${apps.length} app(s) found. Use --appName and --keyWord to search logs, e.g.:`);
|
|
506
|
+
for (const app of apps.slice(0, 3)) {
|
|
507
|
+
console.log(` ${GREEN}yue digger history --appName ${app.appName} --keyWord <keyword>${RESET}`);
|
|
508
|
+
}
|
|
509
|
+
if (apps.length > 3) {
|
|
510
|
+
console.log(` ${DIM}... and ${apps.length - 3} more${RESET}`);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!appFuzzy) {
|
|
515
|
+
console.error(`\u274C --appName is required when searching logs.`);
|
|
516
|
+
console.error(' Run "yue digger history" without --keyWord to list available apps.');
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
const normalizedInput = appFuzzy.replace(/\./g, "-");
|
|
520
|
+
let selectedApp;
|
|
521
|
+
selectedApp = apps.find((a) => a.appName === normalizedInput);
|
|
522
|
+
if (!selectedApp) {
|
|
523
|
+
selectedApp = apps.find((a) => a.appName === appFuzzy);
|
|
524
|
+
}
|
|
525
|
+
if (!selectedApp) {
|
|
526
|
+
selectedApp = apps.find((a) => a.appName.startsWith(normalizedInput));
|
|
527
|
+
}
|
|
528
|
+
if (!selectedApp) {
|
|
529
|
+
selectedApp = apps.find((a) => a.appName.startsWith(searchPrefix));
|
|
530
|
+
}
|
|
531
|
+
if (!selectedApp) {
|
|
532
|
+
selectedApp = apps[0];
|
|
533
|
+
}
|
|
534
|
+
if (apps.length > 1) {
|
|
535
|
+
console.log(`
|
|
536
|
+
\u{1F4CB} Found ${apps.length} matching apps:`);
|
|
537
|
+
for (const app of apps) {
|
|
538
|
+
const marker = app.appName === selectedApp.appName ? " \u2190 selected" : "";
|
|
539
|
+
const desc2 = app.appDesc ? ` \u2014 ${app.appDesc}` : "";
|
|
540
|
+
console.log(` - ${app.appName}${desc2}${marker}`);
|
|
541
|
+
}
|
|
542
|
+
console.log();
|
|
543
|
+
}
|
|
544
|
+
const desc = selectedApp.appDesc ? ` (${selectedApp.appDesc})` : "";
|
|
545
|
+
console.log(`\u2705 Using app: ${selectedApp.appName}${desc}`);
|
|
546
|
+
const logAppName = selectedApp.appName.replace(/-/g, ".");
|
|
547
|
+
const timeDesc = startTime || endTime ? ` from ${opts.startTime || "1h ago"} to ${opts.endTime || "now"}` : "";
|
|
548
|
+
console.log(`\u{1F50E} Searching logs for "${keyword}" in ${logAppName}${timeDesc}...`);
|
|
549
|
+
let result;
|
|
550
|
+
try {
|
|
551
|
+
result = await searchLogs(page, {
|
|
552
|
+
appName: logAppName,
|
|
553
|
+
keyword,
|
|
554
|
+
limit,
|
|
555
|
+
logLevel,
|
|
556
|
+
searchType,
|
|
557
|
+
startTime,
|
|
558
|
+
endTime
|
|
559
|
+
});
|
|
560
|
+
} catch (err) {
|
|
561
|
+
console.error(`\u274C Log search failed: ${err.message}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
if (!result.entries || result.entries.length === 0) {
|
|
565
|
+
console.log("\u{1F4ED} No log entries found.");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (format === "json") {
|
|
569
|
+
console.log(JSON.stringify(result.entries, null, 2));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
for (let i = 0; i < result.entries.length; i++) {
|
|
573
|
+
const entry = result.entries[i];
|
|
574
|
+
const time = formatTimestamp(entry.timestamp);
|
|
575
|
+
const level = colorizeLevel(entry.level);
|
|
576
|
+
const host = entry.host ? `${CYAN}${entry.host}${RESET}` : "";
|
|
577
|
+
console.log("");
|
|
578
|
+
console.log(`${DIM}[${i + 1}]${RESET} ${time} ${level} ${host}`);
|
|
579
|
+
console.log(entry.message || "-");
|
|
580
|
+
}
|
|
581
|
+
console.log(`
|
|
582
|
+
\u{1F4C4} ${result.entries.length} log entry(s) shown (total: ${result.total ?? result.entries.length})`);
|
|
583
|
+
}, { targetUrl: "https://joywatch.jd.com/digger/techApp/overview" });
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/cli.ts
|
|
588
|
+
function run() {
|
|
589
|
+
const program = new Command();
|
|
590
|
+
program.name("yue").description("CLI tool for mydb & Digger \u2014 query databases, search logs").version("0.1.4");
|
|
591
|
+
const mydb = program.command("mydb").description("mydb.jdfmgt.com operations");
|
|
592
|
+
registerDatasourcesCommand(mydb);
|
|
593
|
+
registerQueryCommand(mydb);
|
|
594
|
+
registerDiggerCommand(program);
|
|
595
|
+
program.parse();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// bin/yue.ts
|
|
599
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@p-moon/yue-cli",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool for mydb.jdfmgt.com — query databases and execute SQL via browser session",
|
|
6
|
+
"keywords": ["cli", "mydb", "sql", "database", "jd"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "gaopengfei68",
|
|
9
|
+
"bin": {
|
|
10
|
+
"yue": "./bin/yue.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"bin/yue.mjs"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npx esbuild bin/yue.ts --outfile=dist/bin/yue.js --format=esm --platform=node --target=node18 --bundle --external:@jackwener/opencli --packages=external",
|
|
21
|
+
"dev": "npx tsx bin/yue.ts",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@jackwener/opencli": ">=1.8.5",
|
|
26
|
+
"cli-table3": "^0.6.5",
|
|
27
|
+
"commander": "^14.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|