@lark-apaas/miaoda-cli 0.1.1 → 0.1.2-alpha.2e37617

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.
Files changed (37) hide show
  1. package/dist/api/db/api.js +333 -12
  2. package/dist/api/db/client.js +76 -29
  3. package/dist/api/db/index.js +9 -1
  4. package/dist/api/db/parsers.js +33 -20
  5. package/dist/api/db/sql-keywords.js +123 -0
  6. package/dist/api/file/api.js +93 -24
  7. package/dist/api/file/client.js +1 -5
  8. package/dist/api/file/index.js +2 -1
  9. package/dist/api/file/parsers.js +1 -5
  10. package/dist/api/plugin/api.js +8 -3
  11. package/dist/cli/commands/db/index.js +138 -0
  12. package/dist/cli/commands/file/index.js +7 -0
  13. package/dist/cli/commands/plugin/index.js +18 -6
  14. package/dist/cli/commands/shared.js +1 -3
  15. package/dist/cli/handlers/db/audit.js +285 -0
  16. package/dist/cli/handlers/db/changelog.js +117 -0
  17. package/dist/cli/handlers/db/data.js +23 -3
  18. package/dist/cli/handlers/db/index.js +17 -1
  19. package/dist/cli/handlers/db/migration.js +147 -0
  20. package/dist/cli/handlers/db/quota.js +68 -0
  21. package/dist/cli/handlers/db/recovery.js +188 -0
  22. package/dist/cli/handlers/db/schema.js +22 -8
  23. package/dist/cli/handlers/db/sql.js +304 -16
  24. package/dist/cli/handlers/file/cp.js +39 -17
  25. package/dist/cli/handlers/file/index.js +3 -1
  26. package/dist/cli/handlers/file/ls.js +1 -3
  27. package/dist/cli/handlers/file/quota.js +66 -0
  28. package/dist/cli/handlers/file/rm.js +4 -3
  29. package/dist/cli/handlers/plugin/plugin-local.js +23 -9
  30. package/dist/cli/handlers/plugin/plugin.js +21 -7
  31. package/dist/cli/help.js +5 -2
  32. package/dist/utils/colors.js +98 -0
  33. package/dist/utils/error.js +11 -0
  34. package/dist/utils/fuzzy-match.js +91 -0
  35. package/dist/utils/output.js +81 -5
  36. package/dist/utils/render.js +61 -41
  37. 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
+ ];
@@ -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, { errorContext: "upload callback" });
303
- (0, client_1.ensureSuccess)(body);
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 失败仅 warning,文件已经上传到对象存储
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
- (0, logger_1.debug)(`upload callback failed: ${err instanceof Error ? err.message : String(err)}`);
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
- const remotePath = opts.remotePath ?? `/${opts.fileName}`;
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
- file_name: opts.fileName,
358
- path: remotePath,
359
- size: opts.fileSize,
360
- type: opts.contentType,
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
- // 上传完成后补一次 stat,拿到后端返回的 download_url(preUpload/callback 都不返)
363
- // 非关键路径:拿不到就降级,只是 JSON 输出里缺 download_url 字段
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
+ }
@@ -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
  }
@@ -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; } });
@@ -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
  /**
@@ -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}`, { next_actions: ["检查包名拼写,或确认该插件已在插件市场发布"] });
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}`, { next_actions: ["插件 key 必须形如 @scope/name"] });
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 ?? new error_1.AppError("INTERNAL_RETRY_EXHAUSTED", `${description} failed after ${String(maxRetries)} retries`, { retryable: true, next_actions: ["检查网络后重试,--verbose 可查看重试日志"] });
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 / forever", "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) => { await (0, index_1.handlePluginInstall)({ 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) => { await (0, index_1.handlePluginUpdate)({ 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) => { (0, index_1.handlePluginRemove)({ 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 () => { await (0, index_1.handlePluginInit)(); });
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) => { await (0, index_1.handlePluginList)(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(() => { (0, index_1.handlePluginListPlugins)(); });
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;