@lark-apaas/miaoda-cli 0.1.0-alpha.465bdb8 → 0.1.0-alpha.5f650e8
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/api/db/api.js +60 -22
- package/dist/api/db/client.js +35 -0
- package/dist/api/file/api.js +5 -0
- package/dist/api/file/client.js +36 -0
- package/dist/cli/commands/db/index.js +135 -32
- package/dist/cli/commands/file/index.js +132 -35
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +4 -10
- package/dist/cli/handlers/db/data.js +5 -4
- package/dist/cli/handlers/db/sql.js +28 -3
- package/dist/cli/handlers/file/ls.js +2 -1
- package/dist/cli/handlers/file/rm.js +2 -2
- package/dist/cli/help.js +84 -0
- package/dist/main.js +8 -0
- package/dist/utils/http.js +1 -1
- package/package.json +2 -2
package/dist/api/db/api.js
CHANGED
|
@@ -24,7 +24,9 @@ async function execSql(opts) {
|
|
|
24
24
|
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
|
|
25
25
|
dbBranch: opts.dbBranch,
|
|
26
26
|
});
|
|
27
|
+
const start = Date.now();
|
|
27
28
|
const response = await client.post(url, { sql: opts.sql });
|
|
29
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
28
30
|
if (!response.ok) {
|
|
29
31
|
// 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
|
|
30
32
|
let body = null;
|
|
@@ -57,7 +59,9 @@ async function getSchema(opts) {
|
|
|
57
59
|
includeStats: opts.includeStats ? "true" : undefined,
|
|
58
60
|
dbBranch: opts.dbBranch,
|
|
59
61
|
});
|
|
62
|
+
const start = Date.now();
|
|
60
63
|
const response = await client.get(url);
|
|
64
|
+
(0, client_1.traceHttp)("GET", url, start, response);
|
|
61
65
|
if (!response.ok) {
|
|
62
66
|
let body = null;
|
|
63
67
|
try {
|
|
@@ -73,28 +77,32 @@ async function getSchema(opts) {
|
|
|
73
77
|
const body = (await response.json());
|
|
74
78
|
return (0, client_1.extractData)(body);
|
|
75
79
|
}
|
|
76
|
-
// ── db data import →
|
|
80
|
+
// ── db data import → InnerAdminImportData ──
|
|
77
81
|
/**
|
|
78
82
|
* 导入文件。
|
|
79
|
-
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
83
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
80
84
|
*
|
|
81
|
-
*
|
|
85
|
+
* 全字段走 JSON body envelope(idl-larkgw 36125a2f):
|
|
86
|
+
* {tableName, format, records, dbBranch?}
|
|
87
|
+
*
|
|
88
|
+
* `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
|
|
89
|
+
* Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
|
|
90
|
+
* 字符串后塞进 envelope 即可。
|
|
82
91
|
*/
|
|
83
92
|
async function importData(opts) {
|
|
84
93
|
const client = (0, http_1.getHttpClient)();
|
|
85
|
-
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import"
|
|
94
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
|
|
95
|
+
const reqBody = {
|
|
86
96
|
tableName: opts.tableName,
|
|
87
97
|
format: opts.format,
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
body: ab,
|
|
97
|
-
});
|
|
98
|
+
records: opts.body.toString("utf8"),
|
|
99
|
+
};
|
|
100
|
+
if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
|
|
101
|
+
reqBody.dbBranch = opts.dbBranch;
|
|
102
|
+
}
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
const response = await client.post(url, reqBody);
|
|
105
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
98
106
|
if (!response.ok) {
|
|
99
107
|
let body = null;
|
|
100
108
|
try {
|
|
@@ -107,31 +115,37 @@ async function importData(opts) {
|
|
|
107
115
|
(0, client_1.extractData)(body);
|
|
108
116
|
throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
|
|
109
117
|
}
|
|
110
|
-
// 后端
|
|
118
|
+
// 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
|
|
111
119
|
const body = (await response.json());
|
|
112
120
|
const data = (0, client_1.extractData)(body);
|
|
113
121
|
return {
|
|
114
122
|
tableName: data.tableName ?? opts.tableName,
|
|
115
|
-
|
|
123
|
+
recordCount: data.recordCount ?? 0,
|
|
116
124
|
durationMs: data.durationMs ?? 0,
|
|
117
125
|
};
|
|
118
126
|
}
|
|
119
|
-
// ── db data export →
|
|
127
|
+
// ── db data export → InnerAdminExportData ──
|
|
120
128
|
/**
|
|
121
129
|
* 导出数据。
|
|
122
|
-
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
|
|
130
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
|
|
123
131
|
*
|
|
124
|
-
*
|
|
132
|
+
* 所有参数(含 limit)均按 IDL `api.query` 走 query string;HTTP 方法是 POST(对齐
|
|
133
|
+
* inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
|
|
134
|
+
* 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
|
|
135
|
+
* 回传,错误仍走 HTTP 4xx/5xx + envelope。
|
|
125
136
|
*/
|
|
126
137
|
async function exportData(opts) {
|
|
127
138
|
const client = (0, http_1.getHttpClient)();
|
|
128
139
|
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
|
|
129
140
|
tableName: opts.tableName,
|
|
130
141
|
format: opts.format,
|
|
142
|
+
limit: String(opts.limit ?? 5000),
|
|
131
143
|
dbBranch: opts.dbBranch,
|
|
132
144
|
});
|
|
133
|
-
|
|
134
|
-
const
|
|
145
|
+
// POST + 空 body:所有业务参数都在 query 里
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
const response = await client.request({ method: "POST", url });
|
|
148
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
135
149
|
if (!response.ok) {
|
|
136
150
|
// 错误路径:body 是 JSON envelope
|
|
137
151
|
let body = null;
|
|
@@ -145,7 +159,8 @@ async function exportData(opts) {
|
|
|
145
159
|
(0, client_1.extractData)(body);
|
|
146
160
|
throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
|
|
147
161
|
}
|
|
148
|
-
// 成功路径:响应 body
|
|
162
|
+
// 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
|
|
163
|
+
// HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
|
|
149
164
|
const contentType = response.headers.get("Content-Type") ??
|
|
150
165
|
(opts.format === "csv" ? "text/csv" : "application/json");
|
|
151
166
|
const ab = await response.arrayBuffer();
|
|
@@ -153,10 +168,33 @@ async function exportData(opts) {
|
|
|
153
168
|
if (buf.length === 0) {
|
|
154
169
|
throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
|
|
155
170
|
}
|
|
171
|
+
// Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
|
|
172
|
+
// InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
|
|
173
|
+
// 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
|
|
174
|
+
if (opts.format === "csv" && /application\/json/i.test(contentType)) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(buf.toString("utf8"));
|
|
177
|
+
if (parsed.status_code != null && parsed.status_code !== "0") {
|
|
178
|
+
// 复用 extractData 的错误映射逻辑(throw AppError)
|
|
179
|
+
(0, client_1.extractData)(parsed);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
// 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
|
|
184
|
+
// 真的是 CSV 文本,继续按成功流程走
|
|
185
|
+
if (err instanceof error_1.AppError)
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
|
|
190
|
+
const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
|
|
191
|
+
const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
|
|
192
|
+
const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
|
|
156
193
|
return {
|
|
157
194
|
tableName: opts.tableName,
|
|
158
195
|
format: opts.format,
|
|
159
196
|
contentType,
|
|
160
197
|
body: buf,
|
|
198
|
+
recordCount,
|
|
161
199
|
};
|
|
162
200
|
}
|
package/dist/api/db/client.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SQLSTATE_MAP = void 0;
|
|
4
|
+
exports.traceHttp = traceHttp;
|
|
4
5
|
exports.ensureInnerSuccess = ensureInnerSuccess;
|
|
5
6
|
exports.extractData = extractData;
|
|
6
7
|
exports.buildInnerUrl = buildInnerUrl;
|
|
7
8
|
const error_1 = require("../../utils/error");
|
|
9
|
+
const logger_1 = require("../../utils/logger");
|
|
10
|
+
/**
|
|
11
|
+
* 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
|
|
12
|
+
*
|
|
13
|
+
* 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
|
|
14
|
+
* 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
|
|
15
|
+
*
|
|
16
|
+
* 使用约定:
|
|
17
|
+
* const start = Date.now();
|
|
18
|
+
* const response = await client.post(url, body);
|
|
19
|
+
* traceHttp("POST", url, start, response);
|
|
20
|
+
* // 或错误路径:traceHttp("POST", url, start, err.response, err)
|
|
21
|
+
*/
|
|
22
|
+
function traceHttp(method, url, start, response, err) {
|
|
23
|
+
// debug() 内部已判断 verbose 开关,这里不重复判断;保持调用点轻量
|
|
24
|
+
try {
|
|
25
|
+
const cost = Date.now() - start;
|
|
26
|
+
const status = response?.status ?? 0;
|
|
27
|
+
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
28
|
+
if (err !== undefined) {
|
|
29
|
+
const errMsg = err instanceof Error
|
|
30
|
+
? err.message
|
|
31
|
+
: typeof err === "string"
|
|
32
|
+
? err
|
|
33
|
+
: JSON.stringify(err);
|
|
34
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// debug 失败不应影响业务,吞掉
|
|
41
|
+
}
|
|
42
|
+
}
|
|
8
43
|
/**
|
|
9
44
|
* 校验 dataloom InnerAPI 响应的 envelope。
|
|
10
45
|
*
|
package/dist/api/file/api.js
CHANGED
|
@@ -280,6 +280,7 @@ async function uploadFile(opts) {
|
|
|
280
280
|
// Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
|
|
281
281
|
// 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
|
|
282
282
|
const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
283
|
+
const uploadStart = Date.now();
|
|
283
284
|
const res = await fetch(pre.uploadURL, {
|
|
284
285
|
method: "PUT",
|
|
285
286
|
headers: {
|
|
@@ -288,6 +289,7 @@ async function uploadFile(opts) {
|
|
|
288
289
|
},
|
|
289
290
|
body: ab,
|
|
290
291
|
});
|
|
292
|
+
(0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
|
|
291
293
|
if (!res.ok) {
|
|
292
294
|
throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
|
|
293
295
|
}
|
|
@@ -364,12 +366,15 @@ async function signDownload(opts) {
|
|
|
364
366
|
*/
|
|
365
367
|
async function downloadFile(opts) {
|
|
366
368
|
let res;
|
|
369
|
+
const downloadStart = Date.now();
|
|
367
370
|
try {
|
|
368
371
|
res = await fetch(opts.signedURL);
|
|
369
372
|
}
|
|
370
373
|
catch (err) {
|
|
374
|
+
(0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
371
375
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
376
|
}
|
|
377
|
+
(0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
|
|
373
378
|
if (!res.ok) {
|
|
374
379
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
|
|
375
380
|
}
|
package/dist/api/file/client.js
CHANGED
|
@@ -8,7 +8,34 @@ exports.doPost = doPost;
|
|
|
8
8
|
exports.doRequest = doRequest;
|
|
9
9
|
const http_1 = require("../../utils/http");
|
|
10
10
|
const error_1 = require("../../utils/error");
|
|
11
|
+
const logger_1 = require("../../utils/logger");
|
|
11
12
|
const http_client_1 = require("@lark-apaas/http-client");
|
|
13
|
+
/**
|
|
14
|
+
* 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
|
|
15
|
+
*
|
|
16
|
+
* 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
|
|
17
|
+
* 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
|
|
18
|
+
*/
|
|
19
|
+
function traceHttp(method, url, start, response, err) {
|
|
20
|
+
try {
|
|
21
|
+
const cost = Date.now() - start;
|
|
22
|
+
const status = response?.status ?? 0;
|
|
23
|
+
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
24
|
+
if (err !== undefined) {
|
|
25
|
+
const errMsg = err instanceof Error
|
|
26
|
+
? err.message
|
|
27
|
+
: typeof err === "string"
|
|
28
|
+
? err
|
|
29
|
+
: JSON.stringify(err);
|
|
30
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// debug 失败不应影响业务,吞掉
|
|
37
|
+
}
|
|
38
|
+
}
|
|
12
39
|
/** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
|
|
13
40
|
const bucketCache = new Map();
|
|
14
41
|
/**
|
|
@@ -130,33 +157,42 @@ async function mapHttpError(err, opts) {
|
|
|
130
157
|
* 通过第三个参数显式传入 getRuntimeHttpClient() 切换。
|
|
131
158
|
*/
|
|
132
159
|
async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
160
|
+
const start = Date.now();
|
|
133
161
|
try {
|
|
134
162
|
const response = await client.get(url);
|
|
163
|
+
traceHttp("GET", url, start, response);
|
|
135
164
|
return (await response.json());
|
|
136
165
|
}
|
|
137
166
|
catch (err) {
|
|
167
|
+
traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
138
168
|
await mapHttpError(err, opts);
|
|
139
169
|
throw err; // 不可达,mapHttpError 必定 throw
|
|
140
170
|
}
|
|
141
171
|
}
|
|
142
172
|
/** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
|
|
143
173
|
async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
174
|
+
const start = Date.now();
|
|
144
175
|
try {
|
|
145
176
|
const response = await client.post(url, body);
|
|
177
|
+
traceHttp("POST", url, start, response);
|
|
146
178
|
return (await response.json());
|
|
147
179
|
}
|
|
148
180
|
catch (err) {
|
|
181
|
+
traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
149
182
|
await mapHttpError(err, opts);
|
|
150
183
|
throw err;
|
|
151
184
|
}
|
|
152
185
|
}
|
|
153
186
|
/** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
|
|
154
187
|
async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
188
|
+
const start = Date.now();
|
|
155
189
|
try {
|
|
156
190
|
const response = await client.request(cfg);
|
|
191
|
+
traceHttp(cfg.method, cfg.url, start, response);
|
|
157
192
|
return (await response.json());
|
|
158
193
|
}
|
|
159
194
|
catch (err) {
|
|
195
|
+
traceHttp(cfg.method, cfg.url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
160
196
|
await mapHttpError(err, opts);
|
|
161
197
|
throw err;
|
|
162
198
|
}
|
|
@@ -2,74 +2,177 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerDbCommands = registerDbCommands;
|
|
4
4
|
const index_1 = require("../../../cli/handlers/db/index");
|
|
5
|
-
const shared_1 = require("../../../cli/commands/shared");
|
|
6
5
|
function registerDbCommands(program) {
|
|
7
6
|
const dbCmd = program
|
|
8
7
|
.command("db")
|
|
9
|
-
.description("
|
|
8
|
+
.description("数据库操作:执行 SQL、查看表结构、导入导出数据")
|
|
9
|
+
.usage("<command> [flags]");
|
|
10
10
|
dbCmd.action(() => {
|
|
11
11
|
dbCmd.outputHelp();
|
|
12
12
|
});
|
|
13
|
+
dbCmd.addHelpText("after", `
|
|
14
|
+
Examples:
|
|
15
|
+
$ miaoda db sql "SELECT * FROM users LIMIT 5"
|
|
16
|
+
$ miaoda db schema list
|
|
17
|
+
$ miaoda db schema get users
|
|
18
|
+
$ miaoda db data import users.csv
|
|
19
|
+
$ miaoda db data export users
|
|
20
|
+
`);
|
|
13
21
|
dbCmd
|
|
14
22
|
.command("sql")
|
|
15
|
-
.description("
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.option("--env <env>", "目标环境(main / dev
|
|
23
|
+
.description("执行 SQL 语句(DDL/DML/SELECT 任意 PG 语句)")
|
|
24
|
+
.usage("[query] [flags]")
|
|
25
|
+
.argument("[query]", "SQL 语句;省略时从 stdin 读取")
|
|
26
|
+
.option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
|
|
19
27
|
.action(async (query, opts) => {
|
|
20
28
|
await (0, index_1.handleDbSql)(query, opts);
|
|
21
|
-
})
|
|
29
|
+
})
|
|
30
|
+
.addHelpText("after", `
|
|
31
|
+
Notes:
|
|
32
|
+
- DML 单语句 affectedRows 与 SELECT 单语句返回行数上限均为 1000,超过即拒绝
|
|
33
|
+
- 多语句以 ; 分隔,失败时错误响应携带 statement_index 定位失败位置
|
|
34
|
+
- 支持用户自管事务 BEGIN / COMMIT / ROLLBACK;事务中途失败服务端会自动 ROLLBACK
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
$ miaoda db sql "SELECT * FROM users LIMIT 3"
|
|
38
|
+
✓ 3 rows
|
|
39
|
+
|
|
40
|
+
$ miaoda db sql "CREATE TABLE t(id int)"
|
|
41
|
+
✓ Statement executed
|
|
42
|
+
|
|
43
|
+
$ cat migration.sql | miaoda db sql
|
|
44
|
+
✓ 5 statements executed
|
|
45
|
+
|
|
46
|
+
$ miaoda db sql "SELECT count(*) FROM orders" --json
|
|
47
|
+
[{"count": 1234}]
|
|
48
|
+
|
|
49
|
+
# 报错:多语句中第 1 条失败
|
|
50
|
+
$ miaoda db sql "CREATE TABLE t(id int); SELECT * FROM no_such"
|
|
51
|
+
Error: TABLE_NOT_FOUND at statement 1
|
|
52
|
+
hint: Run \`miaoda db schema list\` to see existing tables.
|
|
53
|
+
`);
|
|
22
54
|
// schema 二级资源分组
|
|
23
55
|
const schemaCmd = dbCmd
|
|
24
56
|
.command("schema")
|
|
25
|
-
.description("
|
|
57
|
+
.description("查看表结构(list / get)")
|
|
58
|
+
.usage("<command> [flags]");
|
|
26
59
|
schemaCmd.action(() => {
|
|
27
60
|
schemaCmd.outputHelp();
|
|
28
61
|
});
|
|
29
62
|
schemaCmd
|
|
30
63
|
.command("list")
|
|
31
|
-
.description("
|
|
32
|
-
.
|
|
33
|
-
.option("--env <env>", "目标环境(main / dev
|
|
64
|
+
.description("列出当前应用的所有表(含行数估算、占用大小、列数)")
|
|
65
|
+
.usage("[flags]")
|
|
66
|
+
.option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
|
|
34
67
|
.action(async (opts) => {
|
|
35
68
|
await (0, index_1.handleDbSchemaList)(opts);
|
|
36
|
-
})
|
|
69
|
+
})
|
|
70
|
+
.addHelpText("after", `
|
|
71
|
+
Examples:
|
|
72
|
+
$ miaoda db schema list
|
|
73
|
+
name rows size_bytes columns
|
|
74
|
+
users 120 65536 6
|
|
75
|
+
orders 5400 327680 9
|
|
76
|
+
|
|
77
|
+
$ miaoda db schema list --json | jq '.[].name'
|
|
78
|
+
"users"
|
|
79
|
+
"orders"
|
|
80
|
+
|
|
81
|
+
$ miaoda db schema list --env main
|
|
82
|
+
`);
|
|
37
83
|
schemaCmd
|
|
38
84
|
.command("get")
|
|
39
|
-
.description("
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.option("--ddl", "
|
|
43
|
-
.option("--env <env>", "目标环境(main / dev
|
|
85
|
+
.description("查看单张表的字段、索引与建表语句;判断表是否存在也用这条")
|
|
86
|
+
.usage("<table> [flags]")
|
|
87
|
+
.argument("<table>", "表名(不带 schema 前缀)")
|
|
88
|
+
.option("--ddl", "只输出 CREATE TABLE 语句,便于复制 / 备份")
|
|
89
|
+
.option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
|
|
44
90
|
.action(async (table, opts) => {
|
|
45
91
|
await (0, index_1.handleDbSchemaGet)(table, opts);
|
|
46
|
-
})
|
|
92
|
+
})
|
|
93
|
+
.addHelpText("after", `
|
|
94
|
+
Notes:
|
|
95
|
+
- 用作"判断表是否存在"探针时:退出码 0 → 表存在;错误码 TABLE_NOT_FOUND → 表不存在
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
$ miaoda db schema get users
|
|
99
|
+
CREATE TABLE users (
|
|
100
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
101
|
+
...
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
$ miaoda db schema get users --ddl
|
|
105
|
+
CREATE TABLE users ( ... );
|
|
106
|
+
|
|
107
|
+
$ miaoda db schema get users --json
|
|
108
|
+
{"name":"users","columns":[...],"indexes":[...]}
|
|
109
|
+
|
|
110
|
+
# 报错:表不存在
|
|
111
|
+
$ miaoda db schema get no_such
|
|
112
|
+
Error: TABLE_NOT_FOUND
|
|
113
|
+
hint: Run \`miaoda db schema list\` to see all tables.
|
|
114
|
+
`);
|
|
47
115
|
// data 二级资源分组
|
|
48
116
|
const dataCmd = dbCmd
|
|
49
117
|
.command("data")
|
|
50
|
-
.description("
|
|
118
|
+
.description("批量导入与导出数据(import / export)")
|
|
119
|
+
.usage("<command> [flags]");
|
|
51
120
|
dataCmd.action(() => {
|
|
52
121
|
dataCmd.outputHelp();
|
|
53
122
|
});
|
|
54
123
|
dataCmd
|
|
55
124
|
.command("import")
|
|
56
|
-
.description("
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
.option("--table <name>", "
|
|
60
|
-
.option("--format <fmt>", "csv
|
|
125
|
+
.description("把本地 CSV / JSON 文件导入到表(单次最多 5000 行 / 1 MB,全部成功或全部回滚)")
|
|
126
|
+
.usage("<file> [flags]")
|
|
127
|
+
.argument("<file>", "本地文件路径(CSV 或 JSON)")
|
|
128
|
+
.option("--table <name>", "目标表名;缺省按文件名(去扩展名)推断")
|
|
129
|
+
.option("--format <fmt>", "文件格式 csv / json;缺省按文件扩展名推断")
|
|
61
130
|
.action(async (file, opts) => {
|
|
62
131
|
await (0, index_1.handleDbDataImport)(file, opts);
|
|
63
|
-
})
|
|
132
|
+
})
|
|
133
|
+
.addHelpText("after", `
|
|
134
|
+
Notes:
|
|
135
|
+
- 单次最多 5000 行 / 1 MB;超过请拆批
|
|
136
|
+
- 任一行失败 → 整批回滚,错误响应给出失败行号 + 原因
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
$ miaoda db data import users.csv
|
|
140
|
+
✓ Imported 120 rows into 'users'
|
|
141
|
+
|
|
142
|
+
$ miaoda db data import data.json --table customers
|
|
143
|
+
✓ Imported 240 rows into 'customers'
|
|
144
|
+
|
|
145
|
+
$ miaoda db data import dump --table orders --format csv
|
|
146
|
+
✓ Imported 80 rows into 'orders'
|
|
147
|
+
|
|
148
|
+
# 报错:第 5 行格式错误,整批回滚
|
|
149
|
+
$ miaoda db data import broken.csv
|
|
150
|
+
Error: ROW_PARSE_FAILED at row 5
|
|
151
|
+
hint: Check column count and value types match the table schema.
|
|
152
|
+
`);
|
|
64
153
|
dataCmd
|
|
65
154
|
.command("export")
|
|
66
|
-
.description("
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
.option("--format <fmt>", "csv
|
|
70
|
-
.option("-f, --file <path>", "
|
|
71
|
-
.option("--limit <n>", "
|
|
155
|
+
.description("把整张表导出为 CSV / JSON 文件(单次最多 5000 行 / 1 MB)")
|
|
156
|
+
.usage("<table> [flags]")
|
|
157
|
+
.argument("<table>", "表名(不带 schema 前缀)")
|
|
158
|
+
.option("--format <fmt>", "导出格式 csv / json,默认 csv")
|
|
159
|
+
.option("-f, --file <path>", "输出文件路径,默认 <table>.<format>")
|
|
160
|
+
.option("--limit <n>", "最多导出行数(不超过 5000)")
|
|
72
161
|
.action(async (table, opts) => {
|
|
73
162
|
await (0, index_1.handleDbDataExport)(table, opts);
|
|
74
|
-
})
|
|
163
|
+
})
|
|
164
|
+
.addHelpText("after", `
|
|
165
|
+
Examples:
|
|
166
|
+
$ miaoda db data export users
|
|
167
|
+
✓ Exported 120 rows to ./users.csv
|
|
168
|
+
|
|
169
|
+
$ miaoda db data export users --format json
|
|
170
|
+
✓ Exported 120 rows to ./users.json
|
|
171
|
+
|
|
172
|
+
$ miaoda db data export users -f /tmp/u.csv
|
|
173
|
+
✓ Exported 120 rows to /tmp/u.csv
|
|
174
|
+
|
|
175
|
+
$ miaoda db data export users --limit 1000
|
|
176
|
+
✓ Exported 1000 rows to ./users.csv
|
|
177
|
+
`);
|
|
75
178
|
}
|
|
@@ -2,66 +2,163 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerFileCommands = registerFileCommands;
|
|
4
4
|
const index_1 = require("../../../cli/handlers/file/index");
|
|
5
|
-
const
|
|
5
|
+
const error_1 = require("../../../utils/error");
|
|
6
|
+
/**
|
|
7
|
+
* commander option 校验器:把 --limit <n> 解析成正整数(≥1)。
|
|
8
|
+
* 默认值(如 "50")会先经过这里被规范化成 number。
|
|
9
|
+
* 非整数 / 负数 / 0 抛 AppError("ARGS_INVALID"),由 main.ts 的全局 catch
|
|
10
|
+
* 走 emitError,同时 process.exitCode 由 commander 自然为 1。
|
|
11
|
+
*/
|
|
12
|
+
function parsePositiveInt(raw) {
|
|
13
|
+
const n = Number(raw);
|
|
14
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
15
|
+
throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
|
|
16
|
+
}
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
6
19
|
function registerFileCommands(program) {
|
|
7
20
|
const fileCmd = program
|
|
8
21
|
.command("file")
|
|
9
|
-
.description("文件操作:上传、下载、删除、查询")
|
|
22
|
+
.description("文件操作:上传、下载、删除、查询")
|
|
23
|
+
.usage("<command> [flags]");
|
|
10
24
|
fileCmd.action(() => {
|
|
11
25
|
fileCmd.outputHelp();
|
|
12
26
|
});
|
|
27
|
+
fileCmd.addHelpText("after", `
|
|
28
|
+
Examples:
|
|
29
|
+
$ miaoda file ls
|
|
30
|
+
$ miaoda file stat report.pdf
|
|
31
|
+
$ miaoda file cp report.pdf /uploads/
|
|
32
|
+
$ miaoda file cp /uploads/report.pdf .
|
|
33
|
+
$ miaoda file sign report.pdf
|
|
34
|
+
$ miaoda file rm a.txt b.txt -y
|
|
35
|
+
`);
|
|
13
36
|
fileCmd
|
|
14
37
|
.command("ls")
|
|
15
|
-
.description("
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.option("--path <path>", "
|
|
19
|
-
.option("--name <name>", "
|
|
20
|
-
.option("--type <mime>", "按 MIME
|
|
21
|
-
.option("--size-gt <size>", "
|
|
22
|
-
.option("--size-lt <size>", "
|
|
23
|
-
.option("--uploaded-since <time>", "
|
|
24
|
-
.option("--limit <n>", "
|
|
25
|
-
.option("--cursor <token>", "
|
|
26
|
-
.option("--all", "
|
|
38
|
+
.description("列出应用下的文件,支持名称 / 路径 / MIME / 大小 / 时间多维筛选")
|
|
39
|
+
.usage("[query] [flags]")
|
|
40
|
+
.argument("[query]", "可选筛选值:以 / 开头视为路径(精确匹配),否则按文件名匹配")
|
|
41
|
+
.option("--path <path>", "按路径精确匹配")
|
|
42
|
+
.option("--name <name>", "按文件名精确匹配")
|
|
43
|
+
.option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
|
|
44
|
+
.option("--size-gt <size>", "文件大小下限(支持 B/KB/MB/GB)")
|
|
45
|
+
.option("--size-lt <size>", "文件大小上限(支持 B/KB/MB/GB)")
|
|
46
|
+
.option("--uploaded-since <time>", "上传时间下限(ISO 8601)")
|
|
47
|
+
.option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
|
|
48
|
+
.option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
|
|
49
|
+
.option("--all", "自动翻页返回全部结果")
|
|
27
50
|
.action(async (query, opts) => {
|
|
28
51
|
await (0, index_1.handleFileLs)({ ...opts, query });
|
|
29
|
-
})
|
|
52
|
+
})
|
|
53
|
+
.addHelpText("after", `
|
|
54
|
+
Examples:
|
|
55
|
+
$ miaoda file ls
|
|
56
|
+
path size uploaded_at
|
|
57
|
+
/uploads/report.pdf 1.2 MB 2026-04-20 10:00
|
|
58
|
+
/uploads/photo.png 800 KB 2026-04-21 14:30
|
|
59
|
+
|
|
60
|
+
$ miaoda file ls report.pdf
|
|
61
|
+
$ miaoda file ls /uploads/report.pdf
|
|
62
|
+
$ miaoda file ls --type image/png --size-gt 1MB
|
|
63
|
+
$ miaoda file ls --uploaded-since 2026-04-01
|
|
64
|
+
|
|
65
|
+
$ miaoda file ls --all --json | jq '.[].name'
|
|
66
|
+
"report.pdf"
|
|
67
|
+
"photo.png"
|
|
68
|
+
`);
|
|
30
69
|
fileCmd
|
|
31
70
|
.command("stat")
|
|
32
|
-
.description("
|
|
33
|
-
.
|
|
34
|
-
.
|
|
71
|
+
.description("查看文件元数据(不含下载链接,需要链接用 sign)")
|
|
72
|
+
.usage("<file> [flags]")
|
|
73
|
+
.argument("<file>", "文件的路径或文件名(自动识别)")
|
|
35
74
|
.action(async (file, opts) => {
|
|
36
75
|
await (0, index_1.handleFileStat)(file, opts);
|
|
37
|
-
})
|
|
76
|
+
})
|
|
77
|
+
.addHelpText("after", `
|
|
78
|
+
Examples:
|
|
79
|
+
$ miaoda file stat report.pdf
|
|
80
|
+
path: /uploads/report.pdf
|
|
81
|
+
size: 1.2 MB
|
|
82
|
+
type: application/pdf
|
|
83
|
+
uploaded_at: 2026-04-20 10:00
|
|
84
|
+
|
|
85
|
+
$ miaoda file stat /uploads/report.pdf
|
|
86
|
+
$ miaoda file stat report.pdf --json
|
|
87
|
+
{"path":"/uploads/report.pdf","size":1258291,...}
|
|
88
|
+
|
|
89
|
+
# 报错:文件不存在
|
|
90
|
+
$ miaoda file stat no_such.pdf
|
|
91
|
+
Error: FILE_NOT_FOUND
|
|
92
|
+
hint: Run \`miaoda file ls\` to see available files.
|
|
93
|
+
`);
|
|
38
94
|
fileCmd
|
|
39
95
|
.command("cp")
|
|
40
|
-
.description("
|
|
41
|
-
.
|
|
96
|
+
.description("上传或下载文件,按 src/dst 自动判断方向")
|
|
97
|
+
.usage("<src> <dst> [flags]")
|
|
98
|
+
.argument("<src>", "源:本地文件路径 或 远程文件路径/名")
|
|
42
99
|
.argument("<dst>", "目标:本地路径 或 远程路径")
|
|
43
|
-
.
|
|
44
|
-
.option("--rename <name>", "上传后使用的新文件名")
|
|
100
|
+
.option("--rename <name>", "上传后在远端使用的新文件名")
|
|
45
101
|
.action(async (src, dst, opts) => {
|
|
46
102
|
await (0, index_1.handleFileCp)(src, dst, opts);
|
|
47
|
-
})
|
|
103
|
+
})
|
|
104
|
+
.addHelpText("after", `
|
|
105
|
+
Notes:
|
|
106
|
+
- src 是本地存在的文件 → 上传
|
|
107
|
+
- src 不是本地文件而 dst 看起来是本地路径 → 下载
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
$ miaoda file cp ./report.pdf /uploads/
|
|
111
|
+
✓ Uploaded report.pdf → /uploads/report.pdf
|
|
112
|
+
|
|
113
|
+
$ miaoda file cp ./report.pdf /uploads/ --rename r.pdf
|
|
114
|
+
✓ Uploaded report.pdf → /uploads/r.pdf
|
|
115
|
+
|
|
116
|
+
$ miaoda file cp /uploads/report.pdf .
|
|
117
|
+
✓ Downloaded /uploads/report.pdf → ./report.pdf
|
|
118
|
+
|
|
119
|
+
$ miaoda file cp /uploads/report.pdf ./local.pdf
|
|
120
|
+
✓ Downloaded /uploads/report.pdf → ./local.pdf
|
|
121
|
+
`);
|
|
48
122
|
fileCmd
|
|
49
123
|
.command("rm")
|
|
50
|
-
.description("
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
.option("-n, --name <name>", "
|
|
54
|
-
.option("-y, --yes", "
|
|
124
|
+
.description("批量删除文件(单次最多 100 个,部分失败不影响其他)")
|
|
125
|
+
.usage("[paths...] [flags]")
|
|
126
|
+
.argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
|
|
127
|
+
.option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
|
|
128
|
+
.option("-y, --yes", "跳过交互确认(脚本 / agent 调用必加)")
|
|
55
129
|
.action(async (paths, opts) => {
|
|
56
130
|
await (0, index_1.handleFileRm)(paths, opts);
|
|
57
|
-
})
|
|
131
|
+
})
|
|
132
|
+
.addHelpText("after", `
|
|
133
|
+
Notes:
|
|
134
|
+
- 单次最多 100 个;超过请拆批
|
|
135
|
+
- 部分失败响应里会列出每条 success / failed 状态,整批仍 exit 0
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
$ miaoda file rm /uploads/a.pdf /uploads/b.pdf -y
|
|
139
|
+
✓ Deleted 2 files
|
|
140
|
+
|
|
141
|
+
$ miaoda file rm a.pdf b.pdf -y
|
|
142
|
+
✓ Deleted 2 files
|
|
143
|
+
|
|
144
|
+
$ miaoda file rm -n a.pdf -n b.pdf -y
|
|
145
|
+
✓ Deleted 2 files
|
|
146
|
+
`);
|
|
58
147
|
fileCmd
|
|
59
148
|
.command("sign")
|
|
60
|
-
.description("
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.option("--expires <duration>", "
|
|
149
|
+
.description("生成可分享的临时下载链接")
|
|
150
|
+
.usage("<file> [flags]")
|
|
151
|
+
.argument("<file>", "文件的路径或文件名")
|
|
152
|
+
.option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 单位(默认 7d,最长 30d)")
|
|
64
153
|
.action(async (file, opts) => {
|
|
65
154
|
await (0, index_1.handleFileSign)(file, opts);
|
|
66
|
-
})
|
|
155
|
+
})
|
|
156
|
+
.addHelpText("after", `
|
|
157
|
+
Examples:
|
|
158
|
+
$ miaoda file sign report.pdf
|
|
159
|
+
https://...?expires=... (valid 7d)
|
|
160
|
+
|
|
161
|
+
$ miaoda file sign report.pdf --expires 30m
|
|
162
|
+
$ miaoda file sign report.pdf --expires 24h
|
|
163
|
+
`);
|
|
67
164
|
}
|
|
@@ -5,7 +5,8 @@ const index_1 = require("../../../cli/handlers/plugin/index");
|
|
|
5
5
|
function registerPluginCommands(program) {
|
|
6
6
|
const pluginCmd = program
|
|
7
7
|
.command("plugin")
|
|
8
|
-
.description("插件管理:安装/更新/移除插件包,查询 capability 实例")
|
|
8
|
+
.description("插件管理:安装/更新/移除插件包,查询 capability 实例")
|
|
9
|
+
.usage("<command> [flags]");
|
|
9
10
|
pluginCmd.action(() => {
|
|
10
11
|
pluginCmd.outputHelp();
|
|
11
12
|
});
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.appIdOption = appIdOption;
|
|
4
3
|
exports.softRequiredOption = softRequiredOption;
|
|
5
4
|
exports.resolveAppId = resolveAppId;
|
|
6
5
|
exports.withHelp = withHelp;
|
|
7
6
|
exports.failArgs = failArgs;
|
|
8
7
|
const commander_1 = require("commander");
|
|
9
8
|
const error_1 = require("../../utils/error");
|
|
10
|
-
/** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption())。
|
|
11
|
-
* Commander 的 .env() 只接受单个变量名,第二个兜底 `app_id` 在 resolveAppId 里手动检查。
|
|
12
|
-
*/
|
|
13
|
-
function appIdOption() {
|
|
14
|
-
return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
|
|
15
|
-
}
|
|
16
9
|
/**
|
|
17
10
|
* soft-required: Commander 类型上 optional,runtime 校验必填。
|
|
18
11
|
*/
|
|
@@ -20,15 +13,16 @@ function softRequiredOption(name, desc) {
|
|
|
20
13
|
return new commander_1.Option(name, desc);
|
|
21
14
|
}
|
|
22
15
|
/**
|
|
23
|
-
* 解析 appId
|
|
24
|
-
*
|
|
16
|
+
* 解析 appId,从环境变量 MIAODA_APP_ID 或 app_id(小写下划线,部分外部沙箱注入)读取。
|
|
17
|
+
* 缺失时抛 APP_ID_MISSING,由全局 catch 处理。
|
|
18
|
+
*
|
|
19
|
+
* opts 里仍保留 appId(可选),用于测试 / 高级场景显式注入;正常 CLI 不暴露此 flag。
|
|
25
20
|
*/
|
|
26
21
|
function resolveAppId(opts) {
|
|
27
22
|
const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
|
|
28
23
|
if (!id) {
|
|
29
24
|
throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
|
|
30
25
|
next_actions: [
|
|
31
|
-
"传入 --app-id <id>",
|
|
32
26
|
"设置 export MIAODA_APP_ID=<id>",
|
|
33
27
|
],
|
|
34
28
|
});
|
|
@@ -82,15 +82,15 @@ async function handleDbDataImport(file, opts) {
|
|
|
82
82
|
data: {
|
|
83
83
|
file,
|
|
84
84
|
table: result.tableName,
|
|
85
|
-
rows: result.
|
|
85
|
+
rows: result.recordCount,
|
|
86
86
|
},
|
|
87
87
|
});
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
const tty = (0, render_1.isStdoutTty)();
|
|
91
91
|
(0, output_1.emit)(tty
|
|
92
|
-
? `✓ Imported ${file} → table '${result.tableName}' (${String(result.
|
|
93
|
-
: `OK Imported ${file} -> table '${result.tableName}' (${String(result.
|
|
92
|
+
? `✓ Imported ${file} → table '${result.tableName}' (${String(result.recordCount)} rows)`
|
|
93
|
+
: `OK Imported ${file} -> table '${result.tableName}' (${String(result.recordCount)} rows)`);
|
|
94
94
|
}
|
|
95
95
|
async function handleDbDataExport(table, opts) {
|
|
96
96
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
@@ -110,7 +110,8 @@ async function handleDbDataExport(table, opts) {
|
|
|
110
110
|
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, { next_actions: [`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`] });
|
|
111
111
|
}
|
|
112
112
|
await fs.writeFile(outputPath, result.body);
|
|
113
|
-
|
|
113
|
+
// 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
|
|
114
|
+
const rows = result.recordCount ?? countRows(result.body, format);
|
|
114
115
|
if ((0, output_1.isJsonMode)()) {
|
|
115
116
|
(0, output_1.emit)({
|
|
116
117
|
data: {
|
|
@@ -37,6 +37,7 @@ exports.handleDbSql = handleDbSql;
|
|
|
37
37
|
const api = __importStar(require("../../../api/index"));
|
|
38
38
|
const error_1 = require("../../../utils/error");
|
|
39
39
|
const output_1 = require("../../../utils/output");
|
|
40
|
+
const config_1 = require("../../../utils/config");
|
|
40
41
|
const shared_1 = require("../../../cli/commands/shared");
|
|
41
42
|
const render_1 = require("../../../utils/render");
|
|
42
43
|
const index_1 = require("../../../api/db/index");
|
|
@@ -125,8 +126,8 @@ function renderSingle(raw) {
|
|
|
125
126
|
}
|
|
126
127
|
function toJson(parsed) {
|
|
127
128
|
if (parsed.kind === "select") {
|
|
128
|
-
// PRD 单 SELECT:data
|
|
129
|
-
return { data: parsed.rows };
|
|
129
|
+
// PRD 单 SELECT:data 直接是行数组(按 --json 字段投影裁剪)
|
|
130
|
+
return { data: projectRows(parsed.rows) };
|
|
130
131
|
}
|
|
131
132
|
if (parsed.kind === "dml") {
|
|
132
133
|
// PRD 单 DML:data = {command, rows_affected}
|
|
@@ -147,7 +148,7 @@ function toJson(parsed) {
|
|
|
147
148
|
*/
|
|
148
149
|
function toMultiElement(parsed) {
|
|
149
150
|
if (parsed.kind === "select") {
|
|
150
|
-
return { command: "SELECT", rows: parsed.rows };
|
|
151
|
+
return { command: "SELECT", rows: projectRows(parsed.rows) };
|
|
151
152
|
}
|
|
152
153
|
if (parsed.kind === "dml") {
|
|
153
154
|
return { command: parsed.sqlType, rows_affected: parsed.affectedRows };
|
|
@@ -155,6 +156,30 @@ function toMultiElement(parsed) {
|
|
|
155
156
|
// DDL:用后端给的细粒度 command
|
|
156
157
|
return { command: parsed.sqlType };
|
|
157
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* PRD:`--json id,name` 字段投影。--json 不带值(boolean true)等价于不裁剪。
|
|
161
|
+
* 字段不存在时按 undefined 处理(JSON.stringify 会忽略 undefined value 的 key),
|
|
162
|
+
* 这样 Agent 拿到的 row 永远只含请求过的列。
|
|
163
|
+
*/
|
|
164
|
+
function projectRows(rows) {
|
|
165
|
+
const fields = parseJsonFields();
|
|
166
|
+
if (!fields)
|
|
167
|
+
return rows;
|
|
168
|
+
return rows.map((r) => {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const f of fields) {
|
|
171
|
+
out[f] = r[f];
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** 读取 --json [fields]:返回字段列表;boolean true 或 undefined 返回 null(不裁剪)。 */
|
|
177
|
+
function parseJsonFields() {
|
|
178
|
+
const v = (0, config_1.getConfig)().json;
|
|
179
|
+
if (typeof v !== "string" || v === "")
|
|
180
|
+
return null;
|
|
181
|
+
return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
182
|
+
}
|
|
158
183
|
/** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
|
|
159
184
|
function renderMultiPretty(results) {
|
|
160
185
|
const tty = (0, render_1.isStdoutTty)();
|
|
@@ -66,7 +66,8 @@ function resolveQueryRouting(opts) {
|
|
|
66
66
|
}
|
|
67
67
|
async function handleFileLs(opts) {
|
|
68
68
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
69
|
-
|
|
69
|
+
// commander 已经把 --limit 解析为 number;保留 ?? undefined 兼容老调用
|
|
70
|
+
const limit = opts.limit;
|
|
70
71
|
const sizeGt = opts.sizeGt ? (0, render_1.parseSize)(opts.sizeGt) : undefined;
|
|
71
72
|
const sizeLt = opts.sizeLt ? (0, render_1.parseSize)(opts.sizeLt) : undefined;
|
|
72
73
|
const { path, name } = resolveQueryRouting(opts);
|
|
@@ -44,7 +44,7 @@ const shared_1 = require("../../../cli/commands/shared");
|
|
|
44
44
|
const index_1 = require("../../../api/file/index");
|
|
45
45
|
const render_1 = require("../../../utils/render");
|
|
46
46
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
47
|
-
const MAX_BATCH =
|
|
47
|
+
const MAX_BATCH = 100;
|
|
48
48
|
/**
|
|
49
49
|
* 解析位置参数(自动识别 path / file_name)与 `--name`(强制 file_name)两类输入。
|
|
50
50
|
*
|
|
@@ -130,7 +130,7 @@ async function handleFileRm(paths, opts) {
|
|
|
130
130
|
throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
|
|
131
131
|
}
|
|
132
132
|
if (totalCount > MAX_BATCH) {
|
|
133
|
-
throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the
|
|
133
|
+
throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
|
|
134
134
|
}
|
|
135
135
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
136
136
|
// destructive guardrail
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MiaodaHelp = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
/**
|
|
6
|
+
* MiaodaHelp 重写 commander 默认的 --help 输出,使之对齐 CLI 文档规范:
|
|
7
|
+
*
|
|
8
|
+
* 1. 描述放在最前(commander 默认是 Usage 在前、描述在后)
|
|
9
|
+
* 2. "Options:" 重命名为 "Flags:","Global Options:" 重命名为 "Global Flags:"
|
|
10
|
+
* 3. Usage 段独占一行 Heading + 缩进展示 usage 行
|
|
11
|
+
* 4. 段落顺序:描述 → Usage → Arguments → Flags → Global Flags → Commands → addHelpText('after')
|
|
12
|
+
*
|
|
13
|
+
* Notes / Examples 段由各命令通过 addHelpText('after', ...) 自行追加,
|
|
14
|
+
* 本类不直接生成 —— 框架与文案分层。
|
|
15
|
+
*/
|
|
16
|
+
class MiaodaHelp extends commander_1.Help {
|
|
17
|
+
// 全局默认开启:所有子命令 --help 都展示 Global Flags 段
|
|
18
|
+
showGlobalOptions = true;
|
|
19
|
+
/**
|
|
20
|
+
* 父级 --help 的 Commands 列表里展示子命令调用形态。规范要求 "<args> [flags]"
|
|
21
|
+
* 顺序,但 commander 默认 subcommandTerm 是 "name [options] <args>"。
|
|
22
|
+
*
|
|
23
|
+
* - 子命令是分组(含下级 subcommand)→ 只显示 name,不带 args/flags
|
|
24
|
+
* - 子命令是 leaf 且配置了 usage() → "name <usage>",对齐 args 在前 / flags 在后
|
|
25
|
+
* - leaf 没配置 usage → 退回 commander 默认行为
|
|
26
|
+
*/
|
|
27
|
+
subcommandTerm(cmd) {
|
|
28
|
+
if (cmd.commands.length > 0) {
|
|
29
|
+
return cmd.name();
|
|
30
|
+
}
|
|
31
|
+
const usage = cmd.usage();
|
|
32
|
+
if (usage) {
|
|
33
|
+
return `${cmd.name()} ${usage}`.trim();
|
|
34
|
+
}
|
|
35
|
+
return super.subcommandTerm(cmd);
|
|
36
|
+
}
|
|
37
|
+
formatHelp(cmd, helper) {
|
|
38
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
39
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
40
|
+
const formatItem = (term, description) => {
|
|
41
|
+
if (description) {
|
|
42
|
+
const padding = " ".repeat(Math.max(termWidth - term.length, 0) + 2);
|
|
43
|
+
return `${term}${padding}${description}`;
|
|
44
|
+
}
|
|
45
|
+
return term;
|
|
46
|
+
};
|
|
47
|
+
const formatList = (lines) => lines.map((l) => " " + l).join("\n");
|
|
48
|
+
void helpWidth; // 保留以备后续按宽度自动 wrap,当前直接透传 description
|
|
49
|
+
const out = [];
|
|
50
|
+
// 1. 描述
|
|
51
|
+
const desc = helper.commandDescription(cmd);
|
|
52
|
+
if (desc) {
|
|
53
|
+
out.push(desc, "");
|
|
54
|
+
}
|
|
55
|
+
// 2. Usage:独立 heading + 缩进
|
|
56
|
+
out.push("Usage:", ` ${helper.commandUsage(cmd)}`, "");
|
|
57
|
+
// 3. Arguments
|
|
58
|
+
const args = helper.visibleArguments(cmd).map((a) => formatItem(helper.argumentTerm(a), helper.argumentDescription(a)));
|
|
59
|
+
if (args.length) {
|
|
60
|
+
out.push("Arguments:", formatList(args), "");
|
|
61
|
+
}
|
|
62
|
+
// 4. Flags(原 Options)
|
|
63
|
+
const opts = helper.visibleOptions(cmd).map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
|
|
64
|
+
if (opts.length) {
|
|
65
|
+
out.push("Flags:", formatList(opts), "");
|
|
66
|
+
}
|
|
67
|
+
// 5. Global Flags(原 Global Options,showGlobalOptions=true 时启用)
|
|
68
|
+
if (this.showGlobalOptions) {
|
|
69
|
+
const globals = helper.visibleGlobalOptions(cmd).map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
|
|
70
|
+
if (globals.length) {
|
|
71
|
+
out.push("Global Flags:", formatList(globals), "");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 6. Commands(仅父级命令组有)
|
|
75
|
+
const subs = helper.visibleCommands(cmd).map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
|
|
76
|
+
if (subs.length) {
|
|
77
|
+
out.push("Commands:", formatList(subs), "");
|
|
78
|
+
}
|
|
79
|
+
// 保留末尾换行:commander 用 join('\n') 拼 addHelpText('after') 段,
|
|
80
|
+
// 这里多留一个 \n,让 Notes / Examples 段与 Flags / Global Flags 段之间空一行。
|
|
81
|
+
return out.join("\n").replace(/\n+$/, "\n");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.MiaodaHelp = MiaodaHelp;
|
package/dist/main.js
CHANGED
|
@@ -5,15 +5,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const commander_1 = require("commander");
|
|
7
7
|
const index_1 = require("./cli/commands/index");
|
|
8
|
+
const help_1 = require("./cli/help");
|
|
8
9
|
const config_1 = require("./utils/config");
|
|
9
10
|
const log_id_1 = require("./utils/log_id");
|
|
10
11
|
const output_1 = require("./utils/output");
|
|
11
12
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
13
|
+
// MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
|
|
14
|
+
// 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
|
|
15
|
+
// 子命令)统一走 MiaodaHelp 渲染,避免在每个子命令上重复 configureHelp。
|
|
16
|
+
commander_1.Command.prototype.createHelp = function () {
|
|
17
|
+
return Object.assign(new help_1.MiaodaHelp(), this.configureHelp());
|
|
18
|
+
};
|
|
12
19
|
const program = new commander_1.Command();
|
|
13
20
|
const { version } = package_json_1.default;
|
|
14
21
|
program
|
|
15
22
|
.name("miaoda")
|
|
16
23
|
.description("妙搭平台命令行工具")
|
|
24
|
+
.usage("<command> [flags]")
|
|
17
25
|
.version(version, "-v, --version", "显示版本号")
|
|
18
26
|
.option("--json [fields]", "JSON 输出,可选字段级选择")
|
|
19
27
|
.option("--output <format>", "输出格式(pretty|json)", "pretty")
|
package/dist/utils/http.js
CHANGED
|
@@ -12,7 +12,7 @@ let runtimeClient;
|
|
|
12
12
|
/**
|
|
13
13
|
* 获取单例 HttpClient(默认:管理端 innerapi)。
|
|
14
14
|
*
|
|
15
|
-
* 管理端链路仅适用于妙搭开发态——baseURL 从 `
|
|
15
|
+
* 管理端链路仅适用于妙搭开发态——baseURL 从 `MIAODA_DEV_INNER_DOMAIN_WITH_PREFIX` 读取,
|
|
16
16
|
* 每次请求自动从 `MIAODA_AUTHN_CODE` 读取用户凭证并注入 `X-Miaoda-Client-Token`。
|
|
17
17
|
* AK/SK 的 `Authorization` / `x-api-key` 照旧叠加。
|
|
18
18
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5f650e8",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"node": ">=20"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lark-apaas/http-client": "^0.1.
|
|
28
|
+
"@lark-apaas/http-client": "^0.1.5",
|
|
29
29
|
"commander": "^13.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|