@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 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();
@@ -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
+ }