@lark-apaas/miaoda-cli 0.1.1 → 0.1.2-alpha.4d0ff57
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 +264 -12
- package/dist/api/db/client.js +76 -29
- package/dist/api/db/index.js +7 -1
- package/dist/api/db/parsers.js +33 -20
- package/dist/api/db/sql-keywords.js +123 -0
- package/dist/api/file/api.js +93 -24
- package/dist/api/file/client.js +1 -5
- package/dist/api/file/index.js +2 -1
- package/dist/api/file/parsers.js +1 -5
- package/dist/api/plugin/api.js +8 -3
- package/dist/cli/commands/db/index.js +138 -0
- package/dist/cli/commands/file/index.js +7 -0
- package/dist/cli/commands/plugin/index.js +18 -6
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/handlers/db/audit.js +250 -0
- package/dist/cli/handlers/db/changelog.js +104 -0
- package/dist/cli/handlers/db/data.js +23 -3
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +127 -0
- package/dist/cli/handlers/db/quota.js +60 -0
- package/dist/cli/handlers/db/recovery.js +141 -0
- package/dist/cli/handlers/db/schema.js +22 -8
- package/dist/cli/handlers/db/sql.js +304 -16
- package/dist/cli/handlers/file/cp.js +39 -17
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +1 -3
- package/dist/cli/handlers/file/quota.js +58 -0
- package/dist/cli/handlers/file/rm.js +4 -3
- package/dist/cli/handlers/plugin/plugin-local.js +23 -9
- package/dist/cli/handlers/plugin/plugin.js +21 -7
- package/dist/cli/help.js +5 -2
- package/dist/utils/colors.js +98 -0
- package/dist/utils/error.js +11 -0
- package/dist/utils/fuzzy-match.js +91 -0
- package/dist/utils/output.js +59 -5
- package/dist/utils/render.js +61 -41
- package/package.json +10 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQL_KEYWORDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 常见 PG SQL 关键字白名单,用于 db sql 拼写错误的 did-you-mean 提示。
|
|
6
|
+
*
|
|
7
|
+
* # 选词原则
|
|
8
|
+
*
|
|
9
|
+
* PG 的关键字总数 700+(见 [pg_keyword 系统视图]),但 CLI 用户实际打错的几乎
|
|
10
|
+
* 都集中在核心动词 / 子句 / 类型 / 事务关键字。这里**少而精**,约 70 个:
|
|
11
|
+
*
|
|
12
|
+
* - DML 动词:SELECT / INSERT / UPDATE / DELETE / MERGE
|
|
13
|
+
* - 子句:FROM / WHERE / JOIN / GROUP / ORDER / HAVING / LIMIT / ...
|
|
14
|
+
* - 操作符词:AND / OR / NOT / IN / IS / NULL / LIKE / ...
|
|
15
|
+
* - DDL:CREATE / ALTER / DROP / TABLE / INDEX / VIEW / ...
|
|
16
|
+
* - 约束:PRIMARY / UNIQUE / FOREIGN / REFERENCES / CHECK / DEFAULT / ...
|
|
17
|
+
* - 事务:BEGIN / COMMIT / ROLLBACK / SAVEPOINT / TRANSACTION
|
|
18
|
+
* - 控制:IF / EXISTS / CASE / WHEN / THEN / ELSE / END / WITH / ...
|
|
19
|
+
*
|
|
20
|
+
* **不要往里加冷僻关键字**——候选集越大,短词越容易 false-positive 错配。
|
|
21
|
+
*/
|
|
22
|
+
exports.SQL_KEYWORDS = [
|
|
23
|
+
// DML 动词
|
|
24
|
+
"SELECT",
|
|
25
|
+
"INSERT",
|
|
26
|
+
"UPDATE",
|
|
27
|
+
"DELETE",
|
|
28
|
+
"MERGE",
|
|
29
|
+
// FROM / JOIN 系列
|
|
30
|
+
"FROM",
|
|
31
|
+
"WHERE",
|
|
32
|
+
"JOIN",
|
|
33
|
+
"LEFT",
|
|
34
|
+
"RIGHT",
|
|
35
|
+
"INNER",
|
|
36
|
+
"OUTER",
|
|
37
|
+
"FULL",
|
|
38
|
+
"CROSS",
|
|
39
|
+
"USING",
|
|
40
|
+
// 聚合 / 排序 / 分页
|
|
41
|
+
"GROUP",
|
|
42
|
+
"ORDER",
|
|
43
|
+
"BY",
|
|
44
|
+
"HAVING",
|
|
45
|
+
"LIMIT",
|
|
46
|
+
"OFFSET",
|
|
47
|
+
"FETCH",
|
|
48
|
+
// 集合操作
|
|
49
|
+
"UNION",
|
|
50
|
+
"INTERSECT",
|
|
51
|
+
"EXCEPT",
|
|
52
|
+
"DISTINCT",
|
|
53
|
+
"ALL",
|
|
54
|
+
// 别名 / 关联
|
|
55
|
+
"AS",
|
|
56
|
+
"ON",
|
|
57
|
+
// 操作符词
|
|
58
|
+
"AND",
|
|
59
|
+
"OR",
|
|
60
|
+
"NOT",
|
|
61
|
+
"IN",
|
|
62
|
+
"IS",
|
|
63
|
+
"NULL",
|
|
64
|
+
"TRUE",
|
|
65
|
+
"FALSE",
|
|
66
|
+
"BETWEEN",
|
|
67
|
+
"LIKE",
|
|
68
|
+
"ILIKE",
|
|
69
|
+
"SIMILAR",
|
|
70
|
+
// DDL
|
|
71
|
+
"CREATE",
|
|
72
|
+
"ALTER",
|
|
73
|
+
"DROP",
|
|
74
|
+
"TRUNCATE",
|
|
75
|
+
"RENAME",
|
|
76
|
+
"TABLE",
|
|
77
|
+
"INDEX",
|
|
78
|
+
"VIEW",
|
|
79
|
+
"DATABASE",
|
|
80
|
+
"SCHEMA",
|
|
81
|
+
"COLUMN",
|
|
82
|
+
"CONSTRAINT",
|
|
83
|
+
"SEQUENCE",
|
|
84
|
+
// 约束
|
|
85
|
+
"PRIMARY",
|
|
86
|
+
"KEY",
|
|
87
|
+
"FOREIGN",
|
|
88
|
+
"REFERENCES",
|
|
89
|
+
"UNIQUE",
|
|
90
|
+
"CHECK",
|
|
91
|
+
"DEFAULT",
|
|
92
|
+
// 写入
|
|
93
|
+
"VALUES",
|
|
94
|
+
"SET",
|
|
95
|
+
"RETURNING",
|
|
96
|
+
"INTO",
|
|
97
|
+
// 事务
|
|
98
|
+
"BEGIN",
|
|
99
|
+
"COMMIT",
|
|
100
|
+
"ROLLBACK",
|
|
101
|
+
"SAVEPOINT",
|
|
102
|
+
"TRANSACTION",
|
|
103
|
+
// 控制流 / CTE
|
|
104
|
+
"IF",
|
|
105
|
+
"EXISTS",
|
|
106
|
+
"REPLACE",
|
|
107
|
+
"WITH",
|
|
108
|
+
"RECURSIVE",
|
|
109
|
+
"CASE",
|
|
110
|
+
"WHEN",
|
|
111
|
+
"THEN",
|
|
112
|
+
"ELSE",
|
|
113
|
+
"END",
|
|
114
|
+
// 类型转换 / 时间提取
|
|
115
|
+
"CAST",
|
|
116
|
+
"EXTRACT",
|
|
117
|
+
// 排序方向
|
|
118
|
+
"ASC",
|
|
119
|
+
"DESC",
|
|
120
|
+
"NULLS",
|
|
121
|
+
"FIRST",
|
|
122
|
+
"LAST",
|
|
123
|
+
];
|
package/dist/api/file/api.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.uploadFile = uploadFile;
|
|
|
8
8
|
exports.signDownload = signDownload;
|
|
9
9
|
exports.downloadFile = downloadFile;
|
|
10
10
|
exports.deleteFiles = deleteFiles;
|
|
11
|
+
exports.getStorageQuota = getStorageQuota;
|
|
11
12
|
const error_1 = require("../../utils/error");
|
|
12
13
|
const logger_1 = require("../../utils/logger");
|
|
13
14
|
const client_1 = require("./client");
|
|
@@ -209,9 +210,7 @@ async function resolveInputs(opts) {
|
|
|
209
210
|
return [];
|
|
210
211
|
return Promise.all(opts.inputs.map((input) => {
|
|
211
212
|
const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? "path" : "name");
|
|
212
|
-
return kind === "path"
|
|
213
|
-
? resolveByPath(opts.appId, input)
|
|
214
|
-
: resolveByName(opts.appId, input);
|
|
213
|
+
return kind === "path" ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
|
|
215
214
|
}));
|
|
216
215
|
}
|
|
217
216
|
/** path 输入:HEAD 判存在性,path 在 bucket 内 unique 无需反查。 */
|
|
@@ -297,14 +296,44 @@ async function preUpload(appId, req) {
|
|
|
297
296
|
const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: "pre-upload" });
|
|
298
297
|
return extractEnvelope(body);
|
|
299
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* 调用 upload callback 拿到对象元数据。
|
|
301
|
+
*
|
|
302
|
+
* 网关 IDL 在 metadata 字段加了 api.response.converter = "decode",正常路径下
|
|
303
|
+
* HTTP 响应里的 metadata 已经被网关从字符串解码成对象;这里两种形态都兼容:
|
|
304
|
+
* - object → 直接当 CallbackObjectVO 用(网关解码场景)
|
|
305
|
+
* - string → JSON.parse 出来用(后端原始形态 / 网关行为变化兜底)
|
|
306
|
+
*
|
|
307
|
+
* 解析失败 / metadata 缺失时返回空对象,由 uploadFile 用本地兜底字段填充。
|
|
308
|
+
*/
|
|
300
309
|
async function uploadCallback(appId, req) {
|
|
301
310
|
const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
|
|
302
|
-
const body = await (0, client_1.doPost)(url, req, {
|
|
303
|
-
|
|
311
|
+
const body = await (0, client_1.doPost)(url, req, {
|
|
312
|
+
errorContext: "upload callback",
|
|
313
|
+
});
|
|
314
|
+
const data = extractEnvelope(body);
|
|
315
|
+
const metadata = data.metadata;
|
|
316
|
+
if (!metadata) {
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
if (typeof metadata === "object") {
|
|
320
|
+
return metadata;
|
|
321
|
+
}
|
|
322
|
+
// string 形态兜底
|
|
323
|
+
try {
|
|
324
|
+
return JSON.parse(metadata);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
(0, logger_1.debug)(`upload callback metadata json parse failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
304
330
|
}
|
|
305
331
|
/**
|
|
306
332
|
* 上传文件(3 步:preUpload → PUT uploadURL → callback)。
|
|
307
333
|
* PUT 步骤直接 fetch uploadURL(对象存储直传,绕开框架 HttpClient)。
|
|
334
|
+
*
|
|
335
|
+
* 注意:opts.remotePath 仅作为目录前缀传给服务端;最终对象 key 由服务端唯一生成
|
|
336
|
+
* (形如 "<前缀>/<16位ID>.<扩展名>"),从 preUpload 响应的 filePath 字段取回。
|
|
308
337
|
*/
|
|
309
338
|
async function uploadFile(opts) {
|
|
310
339
|
const pre = await preUpload(opts.appId, {
|
|
@@ -323,11 +352,15 @@ async function uploadFile(opts) {
|
|
|
323
352
|
// 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
|
|
324
353
|
const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
325
354
|
const uploadStart = Date.now();
|
|
355
|
+
// Content-Disposition 用 attachment + filename 编码原始文件名。TOS 会把这个
|
|
356
|
+
// header 作为对象 metadata 存住,服务端 callback 阶段 HeadObject 读回并解析
|
|
357
|
+
// filename 写入 DB。我们要不传 header,服务端走兜底会把 storage key 当文件名。
|
|
326
358
|
const res = await fetch(pre.uploadURL, {
|
|
327
359
|
method: "PUT",
|
|
328
360
|
headers: {
|
|
329
361
|
"Content-Type": opts.contentType,
|
|
330
362
|
"Content-Length": String(opts.fileSize),
|
|
363
|
+
"Content-Disposition": `attachment; filename="${sanitizeFileName(opts.fileName)}"`,
|
|
331
364
|
},
|
|
332
365
|
body: ab,
|
|
333
366
|
});
|
|
@@ -345,34 +378,56 @@ async function uploadFile(opts) {
|
|
|
345
378
|
if (!etag) {
|
|
346
379
|
throw new error_1.AppError("FILE_UPLOAD_FAILED", "Upload PUT returned no ETag");
|
|
347
380
|
}
|
|
348
|
-
// callback
|
|
381
|
+
// callback 返回服务端实际生成的对象元数据(filePath / file_name / download_url)。
|
|
382
|
+
// path 末段是平台生成的 16 位 GID,CLI 无法靠本地信息推断;callback 失败 /
|
|
383
|
+
// metadata.filePath 缺失时直接抛 FILE_UPLOAD_CALLBACK_INCOMPLETE:对象已落
|
|
384
|
+
// TOS(PUT 已成功 + ETag 拿到),但 CLI 拿不到真实 path。把 fileName 写到
|
|
385
|
+
// hint 里引导用户走 `file ls --name` 查实际 path,避免返一个假 path 让后续
|
|
386
|
+
// stat/download/rm 全部 FILE_NOT_FOUND。
|
|
387
|
+
let metadata;
|
|
349
388
|
try {
|
|
350
|
-
await uploadCallback(opts.appId, { uploadID: pre.uploadID, eTag: etag });
|
|
389
|
+
metadata = await uploadCallback(opts.appId, { uploadID: pre.uploadID, eTag: etag });
|
|
351
390
|
}
|
|
352
391
|
catch (err) {
|
|
353
|
-
|
|
392
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
393
|
+
throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", `Upload callback failed: ${reason}; file may already exist in storage`, {
|
|
394
|
+
next_actions: [
|
|
395
|
+
`Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
|
|
396
|
+
],
|
|
397
|
+
});
|
|
354
398
|
}
|
|
355
|
-
|
|
399
|
+
if (!metadata.filePath) {
|
|
400
|
+
throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", "Upload callback returned no filePath; file may already exist in storage", {
|
|
401
|
+
next_actions: [
|
|
402
|
+
`Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
|
|
403
|
+
],
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const path = metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath;
|
|
356
407
|
const result = {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
408
|
+
// 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
|
|
409
|
+
// 与后续 file ls / file stat 返回的展示名保持一致;缺失时降级用本地 fileName。
|
|
410
|
+
file_name: metadata.name ?? opts.fileName,
|
|
411
|
+
path,
|
|
412
|
+
size: metadata.metadata?.contentLength ?? opts.fileSize,
|
|
413
|
+
type: metadata.metadata?.mimeType ?? opts.contentType,
|
|
361
414
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
const info = await statFile({ appId: opts.appId, filePath: remotePath });
|
|
366
|
-
if (info.download_url)
|
|
367
|
-
result.download_url = info.download_url;
|
|
368
|
-
if (info.file_name)
|
|
369
|
-
result.file_name = info.file_name;
|
|
370
|
-
}
|
|
371
|
-
catch (err) {
|
|
372
|
-
(0, logger_1.debug)(`post-upload stat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
415
|
+
if (metadata.downloadURL) {
|
|
416
|
+
result.download_url = metadata.downloadURL;
|
|
373
417
|
}
|
|
374
418
|
return result;
|
|
375
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* 把文件名清理成可安全放进 Content-Disposition `filename="..."` 的形态。
|
|
422
|
+
* 与 fullstack-plugin 的 sanitizeFileName 行为一致:
|
|
423
|
+
* 1. 去掉对 TOS / 文件系统不友好的字符 [: " \ / * ? < > | , ;]
|
|
424
|
+
* 2. encodeURIComponent 把非 ASCII(中文等)做百分号编码,保证 header 合法
|
|
425
|
+
* 3. 处理后为空时退回 "download_file" 兜底
|
|
426
|
+
*/
|
|
427
|
+
function sanitizeFileName(fileName) {
|
|
428
|
+
const illegalChars = /[:"\\/*?<>|,;]/g;
|
|
429
|
+
return encodeURIComponent(fileName.replace(illegalChars, "")) || "download_file";
|
|
430
|
+
}
|
|
376
431
|
// ── 预签下载 URL ──
|
|
377
432
|
/**
|
|
378
433
|
* 获取预签下载 URL。
|
|
@@ -465,3 +520,17 @@ async function deleteFiles(opts) {
|
|
|
465
520
|
}
|
|
466
521
|
return { deleted, failed };
|
|
467
522
|
}
|
|
523
|
+
// ── storage quota ──
|
|
524
|
+
/**
|
|
525
|
+
* 后端:GET /v1/storage/app/{appId}/bucket/{bucketId}/quota
|
|
526
|
+
* 单 bucket 用量;StorageQuotaBytes 暂未对接,CLI 拿到 0 时按 "—" 渲染。
|
|
527
|
+
* bucketId 缺省时走默认 bucket(与 ls / stat / cp 等一致)。
|
|
528
|
+
*/
|
|
529
|
+
async function getStorageQuota(opts) {
|
|
530
|
+
const bucketId = opts.bucketId ?? (await (0, client_1.getDefaultBucketId)(opts.appId));
|
|
531
|
+
const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/quota`;
|
|
532
|
+
const body = await (0, client_1.doGet)(url, {
|
|
533
|
+
errorContext: `fetch storage quota for app '${opts.appId}' bucket '${bucketId}'`,
|
|
534
|
+
});
|
|
535
|
+
return extractEnvelope(body);
|
|
536
|
+
}
|
package/dist/api/file/client.js
CHANGED
|
@@ -22,11 +22,7 @@ function traceHttp(method, url, start, response, err) {
|
|
|
22
22
|
const status = response?.status ?? 0;
|
|
23
23
|
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
24
24
|
if (err !== undefined) {
|
|
25
|
-
const errMsg = err instanceof Error
|
|
26
|
-
? err.message
|
|
27
|
-
: typeof err === "string"
|
|
28
|
-
? err
|
|
29
|
-
: JSON.stringify(err);
|
|
25
|
+
const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
30
26
|
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
31
27
|
return;
|
|
32
28
|
}
|
package/dist/api/file/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
|
|
3
|
+
exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.getStorageQuota = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
|
|
4
4
|
var api_1 = require("./api");
|
|
5
5
|
Object.defineProperty(exports, "listFiles", { enumerable: true, get: function () { return api_1.listFiles; } });
|
|
6
6
|
Object.defineProperty(exports, "statFile", { enumerable: true, get: function () { return api_1.statFile; } });
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function
|
|
|
10
10
|
Object.defineProperty(exports, "deleteFiles", { enumerable: true, get: function () { return api_1.deleteFiles; } });
|
|
11
11
|
Object.defineProperty(exports, "resolveInputs", { enumerable: true, get: function () { return api_1.resolveInputs; } });
|
|
12
12
|
Object.defineProperty(exports, "parseTimeFilterMs", { enumerable: true, get: function () { return api_1.parseTimeFilterMs; } });
|
|
13
|
+
Object.defineProperty(exports, "getStorageQuota", { enumerable: true, get: function () { return api_1.getStorageQuota; } });
|
|
13
14
|
var client_1 = require("./client");
|
|
14
15
|
Object.defineProperty(exports, "getDefaultBucketId", { enumerable: true, get: function () { return client_1.getDefaultBucketId; } });
|
|
15
16
|
Object.defineProperty(exports, "resetBucketCache", { enumerable: true, get: function () { return client_1.resetBucketCache; } });
|
package/dist/api/file/parsers.js
CHANGED
|
@@ -16,11 +16,7 @@ function int(value) {
|
|
|
16
16
|
function readSize(meta) {
|
|
17
17
|
if (!meta)
|
|
18
18
|
return 0;
|
|
19
|
-
const v = meta.size ??
|
|
20
|
-
meta.fileSize ??
|
|
21
|
-
meta.file_size ??
|
|
22
|
-
meta.contentLength ??
|
|
23
|
-
0;
|
|
19
|
+
const v = meta.size ?? meta.fileSize ?? meta.file_size ?? meta.contentLength ?? 0;
|
|
24
20
|
return int(v);
|
|
25
21
|
}
|
|
26
22
|
/**
|
package/dist/api/plugin/api.js
CHANGED
|
@@ -38,7 +38,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
38
38
|
const versions = await getPluginVersions([pluginKey], isLatest);
|
|
39
39
|
const pluginVersions = versions[pluginKey];
|
|
40
40
|
if (!pluginVersions || pluginVersions.length === 0) {
|
|
41
|
-
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
41
|
+
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
42
|
+
next_actions: ["检查包名拼写,或确认该插件已在插件市场发布"],
|
|
43
|
+
});
|
|
42
44
|
}
|
|
43
45
|
if (isLatest) {
|
|
44
46
|
return pluginVersions[0];
|
|
@@ -53,7 +55,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
53
55
|
function parsePluginKey(key) {
|
|
54
56
|
const match = /^(@[^/]+)\/(.+)$/.exec(key);
|
|
55
57
|
if (!match) {
|
|
56
|
-
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
58
|
+
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
59
|
+
next_actions: ["插件 key 必须形如 @scope/name"],
|
|
60
|
+
});
|
|
57
61
|
}
|
|
58
62
|
return { scope: match[1], name: match[2] };
|
|
59
63
|
}
|
|
@@ -91,7 +95,8 @@ async function withRetry(operation, description, maxRetries = MAX_RETRIES) {
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
|
-
throw lastError ??
|
|
98
|
+
throw (lastError ??
|
|
99
|
+
new error_1.AppError("INTERNAL_RETRY_EXHAUSTED", `${description} failed after ${String(maxRetries)} retries`, { retryable: true, next_actions: ["检查网络后重试,--verbose 可查看重试日志"] }));
|
|
95
100
|
}
|
|
96
101
|
/** 插件缓存目录 */
|
|
97
102
|
const PLUGIN_CACHE_DIR = "node_modules/.cache/miaoda-cli/plugins";
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerDbCommands = registerDbCommands;
|
|
4
|
+
const error_1 = require("../../../utils/error");
|
|
4
5
|
const index_1 = require("../../../cli/handlers/db/index");
|
|
6
|
+
function parsePositiveInt(raw) {
|
|
7
|
+
const n = Number(raw);
|
|
8
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
9
|
+
throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
|
|
10
|
+
}
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
5
13
|
function registerDbCommands(program) {
|
|
6
14
|
const dbCmd = program
|
|
7
15
|
.command("db")
|
|
@@ -179,6 +187,7 @@ Examples:
|
|
|
179
187
|
.option("--format <fmt>", "导出格式 csv / json / sql,默认 csv(sql 输出 INSERT 语句,可用 db sql < file.sql 回放)")
|
|
180
188
|
.option("-f, --file <path>", "输出文件路径,默认 <表名>.<格式>")
|
|
181
189
|
.option("--limit <n>", "最多导出行数(不超过 5000)")
|
|
190
|
+
.option("--force", "输出文件已存在时覆盖(默认报错)")
|
|
182
191
|
.action(async function (table) {
|
|
183
192
|
await (0, index_1.handleDbDataExport)(table, this.optsWithGlobals());
|
|
184
193
|
})
|
|
@@ -205,4 +214,133 @@ Examples:
|
|
|
205
214
|
Error: Output file 'users.csv' already exists
|
|
206
215
|
hint: Use -f to specify a different path, or --force to overwrite.
|
|
207
216
|
`);
|
|
217
|
+
// ── changelog ──
|
|
218
|
+
dbCmd
|
|
219
|
+
.command("changelog")
|
|
220
|
+
.summary("查看 DDL 变更历史")
|
|
221
|
+
.description("列出 DDL 变更(CREATE / ALTER / DROP),支持按表、时间窗筛选与游标分页。")
|
|
222
|
+
.usage("[flags]")
|
|
223
|
+
.option("--table <name>", "只看某张表的 DDL 变更")
|
|
224
|
+
.option("--since <time>", "起始时间(含),相对 1h/2d / 日期 YYYY-MM-DD / ISO 8601")
|
|
225
|
+
.option("--until <time>", "结束时间(含),同 --since 格式")
|
|
226
|
+
.option("--limit <n>", "单次返回上限(默认 20)", parsePositiveInt, 20)
|
|
227
|
+
.option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
|
|
228
|
+
.option("--all", "自动翻页直到取完所有记录")
|
|
229
|
+
.action(async function () {
|
|
230
|
+
await (0, index_1.handleDbChangelog)(this.optsWithGlobals());
|
|
231
|
+
});
|
|
232
|
+
// ── audit ──
|
|
233
|
+
const auditCmd = dbCmd
|
|
234
|
+
.command("audit")
|
|
235
|
+
.summary("表级数据审计开关与查询")
|
|
236
|
+
.description("配置表的数据审计 (enable/disable/retention),查询审计状态与日志。")
|
|
237
|
+
.usage("<command> [flags]");
|
|
238
|
+
auditCmd.action(() => {
|
|
239
|
+
auditCmd.outputHelp();
|
|
240
|
+
});
|
|
241
|
+
auditCmd
|
|
242
|
+
.command("status")
|
|
243
|
+
.summary("查看审计开关状态(不传 table 返列表)")
|
|
244
|
+
.usage("[table] [flags]")
|
|
245
|
+
.argument("[table]", "表名;省略时返所有已配置审计的表")
|
|
246
|
+
.action(async function (table) {
|
|
247
|
+
await (0, index_1.handleDbAuditStatus)(table, this.optsWithGlobals());
|
|
248
|
+
});
|
|
249
|
+
auditCmd
|
|
250
|
+
.command("enable")
|
|
251
|
+
.summary("启用表审计")
|
|
252
|
+
.usage("<table> [flags]")
|
|
253
|
+
.argument("<table>", "目标表名")
|
|
254
|
+
.option("--retention <ttl>", "保留时长 7d / 30d / 180d / 360d", "7d")
|
|
255
|
+
.action(async function (table) {
|
|
256
|
+
await (0, index_1.handleDbAuditEnable)(table, this.optsWithGlobals());
|
|
257
|
+
});
|
|
258
|
+
auditCmd
|
|
259
|
+
.command("disable")
|
|
260
|
+
.summary("关闭表审计")
|
|
261
|
+
.usage("<table> [flags]")
|
|
262
|
+
.argument("<table>", "目标表名")
|
|
263
|
+
.action(async function (table) {
|
|
264
|
+
await (0, index_1.handleDbAuditDisable)(table, this.optsWithGlobals());
|
|
265
|
+
});
|
|
266
|
+
auditCmd
|
|
267
|
+
.command("list")
|
|
268
|
+
.summary("查询审计日志")
|
|
269
|
+
.description("按表 + 时间窗查询审计日志(INSERT / UPDATE / DELETE)。")
|
|
270
|
+
.usage("<table...> [flags]")
|
|
271
|
+
.argument("<tables...>", "一个或多个表名(多表时空表会汇总到 stderr)")
|
|
272
|
+
.option("--since <time>", "起始时间")
|
|
273
|
+
.option("--until <time>", "结束时间")
|
|
274
|
+
.option("--limit <n>", "单次返回上限(默认 50)", parsePositiveInt, 50)
|
|
275
|
+
.option("--cursor <ts>", "分页游标,传上一页末条 event_time")
|
|
276
|
+
.action(async function (tables) {
|
|
277
|
+
await (0, index_1.handleDbAuditList)(tables, this.optsWithGlobals());
|
|
278
|
+
});
|
|
279
|
+
// ── migration ──
|
|
280
|
+
const migrationCmd = dbCmd
|
|
281
|
+
.command("migration")
|
|
282
|
+
.summary("多环境管理(dev / online,仅专家模式应用)")
|
|
283
|
+
.description("init 拆分双环境,diff 预览待发布变更,apply 把 dev 同步到 online。")
|
|
284
|
+
.usage("<command> [flags]");
|
|
285
|
+
migrationCmd.action(() => {
|
|
286
|
+
migrationCmd.outputHelp();
|
|
287
|
+
});
|
|
288
|
+
migrationCmd
|
|
289
|
+
.command("init")
|
|
290
|
+
.summary("初始化多环境(不可逆)")
|
|
291
|
+
.usage("[flags]")
|
|
292
|
+
.option("--sync-data", "同时把现有数据复制到 dev")
|
|
293
|
+
.option("--yes", "跳过 TTY 确认")
|
|
294
|
+
.action(async function () {
|
|
295
|
+
await (0, index_1.handleDbMigrationInit)(this.optsWithGlobals());
|
|
296
|
+
});
|
|
297
|
+
migrationCmd
|
|
298
|
+
.command("diff")
|
|
299
|
+
.summary("预览 dev → online 待发布变更")
|
|
300
|
+
.usage("[flags]")
|
|
301
|
+
.action(async function () {
|
|
302
|
+
await (0, index_1.handleDbMigrationDiff)(this.optsWithGlobals());
|
|
303
|
+
});
|
|
304
|
+
migrationCmd
|
|
305
|
+
.command("apply")
|
|
306
|
+
.summary("应用 dev 变更到 online(多 DDL 单事务原子)")
|
|
307
|
+
.usage("[flags]")
|
|
308
|
+
.option("--yes", "跳过 TTY 二次确认")
|
|
309
|
+
.action(async function () {
|
|
310
|
+
await (0, index_1.handleDbMigrationApply)(this.optsWithGlobals());
|
|
311
|
+
});
|
|
312
|
+
// ── recovery(PITR)──
|
|
313
|
+
const recoveryCmd = dbCmd
|
|
314
|
+
.command("recovery")
|
|
315
|
+
.summary("基于时间点恢复(PITR)")
|
|
316
|
+
.description("把数据库整体恢复到指定时间点,diff 预览影响范围,apply 不可逆覆盖。")
|
|
317
|
+
.usage("<command> [flags]");
|
|
318
|
+
recoveryCmd.action(() => {
|
|
319
|
+
recoveryCmd.outputHelp();
|
|
320
|
+
});
|
|
321
|
+
recoveryCmd
|
|
322
|
+
.command("diff")
|
|
323
|
+
.summary("预览恢复到指定时间点的影响范围")
|
|
324
|
+
.usage("<timestamp> [flags]")
|
|
325
|
+
.argument("<timestamp>", "目标时间,YYYY-MM-DD / YYYY-MM-DD HH:MM[:SS] / ISO 8601")
|
|
326
|
+
.action(async function (target) {
|
|
327
|
+
await (0, index_1.handleDbRecoveryDiff)(target, this.optsWithGlobals());
|
|
328
|
+
});
|
|
329
|
+
recoveryCmd
|
|
330
|
+
.command("apply")
|
|
331
|
+
.summary("触发恢复到指定时间点(不可逆覆盖)")
|
|
332
|
+
.usage("<timestamp> [flags]")
|
|
333
|
+
.argument("<timestamp>", "目标时间,YYYY-MM-DD / YYYY-MM-DD HH:MM[:SS] / ISO 8601")
|
|
334
|
+
.option("--yes", "跳过 TTY 二次确认")
|
|
335
|
+
.action(async function (target) {
|
|
336
|
+
await (0, index_1.handleDbRecoveryApply)(target, this.optsWithGlobals());
|
|
337
|
+
});
|
|
338
|
+
// ── quota ──
|
|
339
|
+
dbCmd
|
|
340
|
+
.command("quota")
|
|
341
|
+
.summary("查看数据库用量与限额")
|
|
342
|
+
.usage("[flags]")
|
|
343
|
+
.action(async function () {
|
|
344
|
+
await (0, index_1.handleDbQuota)(this.optsWithGlobals());
|
|
345
|
+
});
|
|
208
346
|
}
|
|
@@ -209,4 +209,11 @@ Examples:
|
|
|
209
209
|
Error: Expires duration '60d' exceeds the maximum of 30d
|
|
210
210
|
hint: Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.
|
|
211
211
|
`);
|
|
212
|
+
fileCmd
|
|
213
|
+
.command("quota")
|
|
214
|
+
.summary("查看文件存储用量与限额")
|
|
215
|
+
.usage("[flags]")
|
|
216
|
+
.action(async function () {
|
|
217
|
+
await (0, index_1.handleFileQuota)(this.optsWithGlobals());
|
|
218
|
+
});
|
|
212
219
|
}
|
|
@@ -58,7 +58,9 @@ JSON 输出
|
|
|
58
58
|
Error: Invalid plugin name format: bad-name. Expected: @scope/name or @scope/name@version
|
|
59
59
|
hint: 示例:@demo/example-plugin 或 @demo/example-plugin@1.2.3
|
|
60
60
|
`)
|
|
61
|
-
.action(async (names) => {
|
|
61
|
+
.action(async (names) => {
|
|
62
|
+
await (0, index_1.handlePluginInstall)({ names });
|
|
63
|
+
});
|
|
62
64
|
pluginCmd
|
|
63
65
|
.command("update")
|
|
64
66
|
.description("把已安装插件升级到 latest 版本")
|
|
@@ -91,7 +93,9 @@ JSON 输出
|
|
|
91
93
|
$ miaoda plugin update @demo/not-installed --json
|
|
92
94
|
{"updated":[],"skipped":[],"notInstalled":["@demo/not-installed"],"failed":[]}
|
|
93
95
|
`)
|
|
94
|
-
.action(async (names) => {
|
|
96
|
+
.action(async (names) => {
|
|
97
|
+
await (0, index_1.handlePluginUpdate)({ names });
|
|
98
|
+
});
|
|
95
99
|
pluginCmd
|
|
96
100
|
.command("remove")
|
|
97
101
|
.description("从当前项目移除一个已安装的插件")
|
|
@@ -115,7 +119,9 @@ JSON 输出
|
|
|
115
119
|
Error: Plugin @demo/not-installed is not installed
|
|
116
120
|
hint: 运行 miaoda plugin list-packages 查看已安装插件
|
|
117
121
|
`)
|
|
118
|
-
.action((name) => {
|
|
122
|
+
.action((name) => {
|
|
123
|
+
(0, index_1.handlePluginRemove)({ name });
|
|
124
|
+
});
|
|
119
125
|
pluginCmd
|
|
120
126
|
.command("init")
|
|
121
127
|
.description("按 package.json 的 actionPlugins 批量安装所有插件")
|
|
@@ -146,7 +152,9 @@ JSON 输出
|
|
|
146
152
|
Error: package.json not found in current directory
|
|
147
153
|
hint: 在应用项目根目录运行
|
|
148
154
|
`)
|
|
149
|
-
.action(async () => {
|
|
155
|
+
.action(async () => {
|
|
156
|
+
await (0, index_1.handlePluginInit)();
|
|
157
|
+
});
|
|
150
158
|
pluginCmd
|
|
151
159
|
.command("list")
|
|
152
160
|
.description("列出当前项目的 capability 实例(./server/capabilities/*.json)")
|
|
@@ -178,7 +186,9 @@ JSON 输出
|
|
|
178
186
|
Error: server/capabilities directory not found
|
|
179
187
|
hint: 当前目录必须是含 server/capabilities/ 的应用项目
|
|
180
188
|
`)
|
|
181
|
-
.action(async (opts) => {
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
await (0, index_1.handlePluginList)(opts);
|
|
191
|
+
});
|
|
182
192
|
pluginCmd
|
|
183
193
|
.command("list-packages")
|
|
184
194
|
.description("列出 package.json actionPlugins 里已声明的插件包")
|
|
@@ -201,5 +211,7 @@ JSON 输出
|
|
|
201
211
|
Error: package.json not found in current directory
|
|
202
212
|
hint: 在应用项目根目录运行
|
|
203
213
|
`)
|
|
204
|
-
.action(() => {
|
|
214
|
+
.action(() => {
|
|
215
|
+
(0, index_1.handlePluginListPlugins)();
|
|
216
|
+
});
|
|
205
217
|
}
|
|
@@ -22,9 +22,7 @@ function resolveAppId(opts) {
|
|
|
22
22
|
const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
|
|
23
23
|
if (!id) {
|
|
24
24
|
throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
|
|
25
|
-
next_actions: [
|
|
26
|
-
"设置 export MIAODA_APP_ID=<id>",
|
|
27
|
-
],
|
|
25
|
+
next_actions: ["设置 export MIAODA_APP_ID=<id>"],
|
|
28
26
|
});
|
|
29
27
|
}
|
|
30
28
|
return id;
|