@lark-apaas/miaoda-cli 0.1.2-alpha.4d0ff57 → 0.1.2-alpha.51197c2

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.
@@ -5,10 +5,14 @@ exports.getSchema = getSchema;
5
5
  exports.importData = importData;
6
6
  exports.exportData = exportData;
7
7
  exports.listDDLChangelog = listDDLChangelog;
8
+ exports.getAuditStatus = getAuditStatus;
8
9
  exports.setAuditConfig = setAuditConfig;
10
+ exports.listAuditLog = listAuditLog;
9
11
  exports.migrationInit = migrationInit;
10
12
  exports.migrate = migrate;
13
+ exports.getMigrationStatus = getMigrationStatus;
11
14
  exports.recover = recover;
15
+ exports.getRecoveryPreview = getRecoveryPreview;
12
16
  exports.getDbQuota = getDbQuota;
13
17
  const http_1 = require("../../utils/http");
14
18
  const error_1 = require("../../utils/error");
@@ -31,6 +35,22 @@ async function mapDbHttpError(err, url, ctx,
31
35
  onErrorBody) {
32
36
  if (err instanceof error_1.AppError)
33
37
  throw err;
38
+ // 客户端超时 / abort:SdkHttpError 时 response 通常缺失(status=0),落到 throw 时
39
+ // message 形如 'Failed to recover: 0' 让用户看不懂。识别 abort / timeout 关键字
40
+ // 转成专用错误码 + 友好 hint。
41
+ if (err instanceof Error) {
42
+ const msg = err.message.toLowerCase();
43
+ if (msg.includes("aborted") ||
44
+ msg.includes("timeout") ||
45
+ err.name === "AbortError" ||
46
+ err.name === "TimeoutError") {
47
+ throw new error_1.AppError("REQUEST_TIMEOUT", `${ctx}: request timed out`, {
48
+ next_actions: [
49
+ "Server-side async tasks may take up to 60s. Retry the command if the underlying task likely succeeded.",
50
+ ],
51
+ });
52
+ }
53
+ }
34
54
  if (err instanceof http_client_1.HttpError) {
35
55
  const status = err.response?.status ?? 0;
36
56
  const statusText = err.response?.statusText ?? "";
@@ -332,13 +352,35 @@ async function listDDLChangelog(opts) {
332
352
  hasMore: Boolean(data.hasMore),
333
353
  };
334
354
  }
335
- // ── db audit → InnerAdminSetAuditConfig ──
355
+ // ── db audit → InnerAdminGetAuditStatus / InnerAdminSetAuditConfig ──
356
+ /**
357
+ * 后端:GET /v1/dataloom/app/{appId}/db/audit/status?table=&dbBranch=
358
+ * 查表审计开关状态。table 非空 → 单表过滤;空 → 返当前 workspace 全部已配置表。
359
+ */
360
+ async function getAuditStatus(opts) {
361
+ const client = (0, http_1.getHttpClient)();
362
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/audit/status", {
363
+ table: opts.table,
364
+ dbBranch: opts.dbBranch,
365
+ });
366
+ const start = Date.now();
367
+ let response;
368
+ try {
369
+ response = await client.get(url);
370
+ (0, client_1.traceHttp)("GET", url, start, response);
371
+ }
372
+ catch (err) {
373
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
374
+ await mapDbHttpError(err, url, "Failed to get audit status");
375
+ throw err; // 不可达
376
+ }
377
+ const respBody = (await response.json());
378
+ const data = (0, client_1.extractData)(respBody);
379
+ return data.items ?? [];
380
+ }
336
381
  /**
337
382
  * 后端:POST /v1/dataloom/app/{appId}/db/audit/config
338
383
  * 写:enabled=true 开启 / false 关闭。retention 仅 enabled=true 生效。
339
- *
340
- * 读路径(status / log)不在此模块——CLI handler 走 execSql 直接 SELECT
341
- * `_dl_audit_config` / `_dl_audit_log` 元数据表。
342
384
  */
343
385
  async function setAuditConfig(opts) {
344
386
  const client = (0, http_1.getHttpClient)();
@@ -369,6 +411,51 @@ async function setAuditConfig(opts) {
369
411
  }
370
412
  return data.status;
371
413
  }
414
+ // ── db audit log → InnerAdminListAuditLog ──
415
+ /**
416
+ * 后端:GET /v1/dataloom/app/{appId}/db/audit/log?tables=&since=&until=&limit=&cursor=&dbBranch=
417
+ *
418
+ * 走 admin-inner 接口而不是 InnerAdminExecuteSQL 直接 SELECT pg_audit:
419
+ * - operator 在 details JSONB 内是 user_id,服务端解析成 username
420
+ * - summary 后端按 type + before/after diff 合成(pg_audit 表无此列)
421
+ * - before/after JSONB 后端 JSON.stringify 后透传字符串,CLI 按需 parse
422
+ *
423
+ * 多表用逗号拼接走 query;后端按 target_table IN (...) 一次查。skipped 字段返
424
+ * 多表中无记录的表名,便于 CLI 展示 hint。
425
+ */
426
+ async function listAuditLog(opts) {
427
+ if (opts.tables.length === 0) {
428
+ throw new error_1.AppError("ARGS_INVALID", "at least one table is required");
429
+ }
430
+ const client = (0, http_1.getHttpClient)();
431
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/audit/log", {
432
+ tables: opts.tables.join(","),
433
+ since: opts.since,
434
+ until: opts.until,
435
+ limit: opts.limit !== undefined ? String(opts.limit) : undefined,
436
+ cursor: opts.cursor,
437
+ dbBranch: opts.dbBranch,
438
+ });
439
+ const start = Date.now();
440
+ let response;
441
+ try {
442
+ response = await client.get(url);
443
+ (0, client_1.traceHttp)("GET", url, start, response);
444
+ }
445
+ catch (err) {
446
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
447
+ await mapDbHttpError(err, url, "Failed to list audit log");
448
+ throw err; // 不可达
449
+ }
450
+ const body = (await response.json());
451
+ const data = (0, client_1.extractData)(body);
452
+ return {
453
+ items: data.items ?? [],
454
+ nextCursor: data.nextCursor && data.nextCursor !== "" ? data.nextCursor : null,
455
+ hasMore: Boolean(data.hasMore),
456
+ skipped: data.skipped ?? [],
457
+ };
458
+ }
372
459
  // ── db migration → InnerAdminMigrationInit / InnerAdminMigrate ──
373
460
  /**
374
461
  * 后端:POST /v1/dataloom/app/{appId}/db/enableMultiEnv
@@ -415,6 +502,31 @@ async function migrate(opts) {
415
502
  const respBody = (await response.json());
416
503
  return (0, client_1.extractData)(respBody);
417
504
  }
505
+ /**
506
+ * 后端:GET /v1/dataloom/app/{appId}/db/migration/status?taskId=...
507
+ * CLI 拿到 migration apply 的 taskId 后定时调本接口,直到 status=success/failed。
508
+ * 网络层超时仍走 mapDbHttpError → 单次 30s;轮询节奏由 CLI handler 自行控制。
509
+ */
510
+ async function getMigrationStatus(opts) {
511
+ const client = (0, http_1.getHttpClient)();
512
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/migration/status", {
513
+ taskId: opts.taskId,
514
+ dbBranch: opts.dbBranch,
515
+ });
516
+ const start = Date.now();
517
+ let response;
518
+ try {
519
+ response = await client.get(url);
520
+ (0, client_1.traceHttp)("GET", url, start, response);
521
+ }
522
+ catch (err) {
523
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
524
+ await mapDbHttpError(err, url, "Failed to get migration status");
525
+ throw err; // 不可达
526
+ }
527
+ const body = (await response.json());
528
+ return (0, client_1.extractData)(body);
529
+ }
418
530
  // ── db recovery → InnerAdminRecover ──
419
531
  /**
420
532
  * 后端:POST /v1/dataloom/app/{appId}/db/recovery
@@ -440,6 +552,30 @@ async function recover(opts) {
440
552
  const respBody = (await response.json());
441
553
  return (0, client_1.extractData)(respBody);
442
554
  }
555
+ /**
556
+ * 后端:GET /v1/dataloom/app/{appId}/db/recovery/preview?previewRequestId=...
557
+ * CLI 拿到 recovery diff 的 previewRequestId 后定时调本接口直到 previewStatus=success/failed。
558
+ */
559
+ async function getRecoveryPreview(opts) {
560
+ const client = (0, http_1.getHttpClient)();
561
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/recovery/preview", {
562
+ previewRequestId: opts.previewRequestId,
563
+ dbBranch: opts.dbBranch,
564
+ });
565
+ const start = Date.now();
566
+ let response;
567
+ try {
568
+ response = await client.get(url);
569
+ (0, client_1.traceHttp)("GET", url, start, response);
570
+ }
571
+ catch (err) {
572
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
573
+ await mapDbHttpError(err, url, "Failed to get recovery preview");
574
+ throw err; // 不可达
575
+ }
576
+ const body = (await response.json());
577
+ return (0, client_1.extractData)(body);
578
+ }
443
579
  // ── db quota → InnerAdminGetDbQuota ──
444
580
  /**
445
581
  * 后端:GET /v1/dataloom/app/{appId}/db/quota?dbBranch=
@@ -140,7 +140,7 @@ const BIZ_ERR_MAP = new Map(Object.entries({
140
140
  },
141
141
  k_dl_1310003: {
142
142
  code: "INVALID_RETENTION",
143
- hint: "Allowed values: 7d / 30d / 180d / 360d.",
143
+ hint: "Allowed values: 7d, 30d, 180d, 360d, forever.",
144
144
  },
145
145
  // migration
146
146
  k_dl_1320001: {
@@ -1,16 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.recover = exports.migrate = exports.migrationInit = exports.setAuditConfig = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
3
+ exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.getRecoveryPreview = exports.recover = exports.getMigrationStatus = exports.migrate = exports.migrationInit = exports.listAuditLog = exports.setAuditConfig = exports.getAuditStatus = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
4
4
  var api_1 = require("./api");
5
5
  Object.defineProperty(exports, "execSql", { enumerable: true, get: function () { return api_1.execSql; } });
6
6
  Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return api_1.getSchema; } });
7
7
  Object.defineProperty(exports, "importData", { enumerable: true, get: function () { return api_1.importData; } });
8
8
  Object.defineProperty(exports, "exportData", { enumerable: true, get: function () { return api_1.exportData; } });
9
9
  Object.defineProperty(exports, "listDDLChangelog", { enumerable: true, get: function () { return api_1.listDDLChangelog; } });
10
+ Object.defineProperty(exports, "getAuditStatus", { enumerable: true, get: function () { return api_1.getAuditStatus; } });
10
11
  Object.defineProperty(exports, "setAuditConfig", { enumerable: true, get: function () { return api_1.setAuditConfig; } });
12
+ Object.defineProperty(exports, "listAuditLog", { enumerable: true, get: function () { return api_1.listAuditLog; } });
11
13
  Object.defineProperty(exports, "migrationInit", { enumerable: true, get: function () { return api_1.migrationInit; } });
12
14
  Object.defineProperty(exports, "migrate", { enumerable: true, get: function () { return api_1.migrate; } });
15
+ Object.defineProperty(exports, "getMigrationStatus", { enumerable: true, get: function () { return api_1.getMigrationStatus; } });
13
16
  Object.defineProperty(exports, "recover", { enumerable: true, get: function () { return api_1.recover; } });
17
+ Object.defineProperty(exports, "getRecoveryPreview", { enumerable: true, get: function () { return api_1.getRecoveryPreview; } });
14
18
  Object.defineProperty(exports, "getDbQuota", { enumerable: true, get: function () { return api_1.getDbQuota; } });
15
19
  var client_1 = require("./client");
16
20
  Object.defineProperty(exports, "buildInnerUrl", { enumerable: true, get: function () { return client_1.buildInnerUrl; } });
@@ -218,17 +218,49 @@ Examples:
218
218
  dbCmd
219
219
  .command("changelog")
220
220
  .summary("查看 DDL 变更历史")
221
- .description("列出 DDL 变更(CREATE / ALTER / DROP),支持按表、时间窗筛选与游标分页。")
221
+ .description("查看 DDL 变更记录(建表、改表、删表等)。默认按时间倒序显示,可按表名或时间范围过滤。")
222
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", "自动翻页直到取完所有记录")
223
+ .option("--table <name>", "按表名过滤")
224
+ .option("--since <time>", "起始时间,支持 YYYY-MM-DD(按当日 00:00:00 UTC)/ ISO 8601(如 2026-04-01T10:00:00Z)/ 相对时间(1h、2d、1w)")
225
+ .option("--until <time>", "截止时间,格式同 --since")
226
+ .option("--limit <n>", "返回条数上限(默认 20)", parsePositiveInt, 20)
227
+ .option("--cursor <token>", "从上一页返回的游标位置继续获取")
228
+ .option("--all", "获取全部结果,自动翻页")
229
229
  .action(async function () {
230
230
  await (0, index_1.handleDbChangelog)(this.optsWithGlobals());
231
- });
231
+ })
232
+ .addHelpText("after", `
233
+ Notes:
234
+ - DDL 变更由系统自动记录,不可关闭,无需单独开启。
235
+ - 默认输出展示摘要(summary),完整 SQL 原文(statement)通过 --json 获取。
236
+
237
+ Examples:
238
+ # 列出近期变更(statement 截断展示,完整 SQL 用 --json 获取)
239
+ $ miaoda db changelog
240
+ change_id changed_at operator target_table change_type summary
241
+ 1862462453263587 2026-04-16 13:24:59 zqy users ALTER_AUDIT_RETENTION 修改记录日志周期
242
+ 1861814222210116 2026-04-08 20:12:37 zqy users CREATE_TABLE 创建并修改数据表
243
+
244
+ # 按条件过滤
245
+ $ miaoda db changelog --table users --since 2026-04-01
246
+ $ miaoda db changelog --limit 5
247
+
248
+ # JSON 输出包含完整 statement
249
+ $ miaoda db changelog --limit 1 --json
250
+ {
251
+ "data": [{
252
+ "change_id": "1861814222210116",
253
+ "changed_at": "2026-04-08T20:12:37Z",
254
+ "operator": "zqy",
255
+ "target_table": "users",
256
+ "change_type": "CREATE_TABLE",
257
+ "summary": "创建并修改数据表",
258
+ "statement": "CREATE TABLE ... ;"
259
+ }],
260
+ "next_cursor": null,
261
+ "has_more": false
262
+ }
263
+ `);
232
264
  // ── audit ──
233
265
  const auditCmd = dbCmd
234
266
  .command("audit")
@@ -240,42 +272,136 @@ Examples:
240
272
  });
241
273
  auditCmd
242
274
  .command("status")
243
- .summary("查看审计开关状态(不传 table 返列表)")
275
+ .summary("查看审计开关状态")
276
+ .description("查看表的审计开关状态。不指定表则显示全部。")
244
277
  .usage("[table] [flags]")
245
278
  .argument("[table]", "表名;省略时返所有已配置审计的表")
246
279
  .action(async function (table) {
247
280
  await (0, index_1.handleDbAuditStatus)(table, this.optsWithGlobals());
248
- });
281
+ })
282
+ .addHelpText("after", `
283
+ Examples:
284
+ # 全部表
285
+ $ miaoda db audit status
286
+ table enabled enabled_at retention
287
+ users yes 2026-04-01 08:00:00 30d
288
+ orders no — —
289
+ products yes 2026-03-15 10:30:00 forever
290
+
291
+ # 单表
292
+ $ miaoda db audit status users
293
+ Table: users
294
+ Enabled: yes
295
+ Enabled at: 2026-04-01 08:00:00
296
+ Retention: 30d
297
+
298
+ # JSON
299
+ $ miaoda db audit status --json
300
+ $ miaoda db audit status users --json
301
+ `);
249
302
  auditCmd
250
303
  .command("enable")
251
304
  .summary("启用表审计")
305
+ .description("开启指定表的审计,记录每次 INSERT / UPDATE / DELETE 操作。")
252
306
  .usage("<table> [flags]")
253
307
  .argument("<table>", "目标表名")
254
- .option("--retention <ttl>", "保留时长 7d / 30d / 180d / 360d", "7d")
308
+ .option("--retention <period>", "审计日志保留时长,可选 7d / 30d / 180d / 360d / forever", "7d")
255
309
  .action(async function (table) {
256
310
  await (0, index_1.handleDbAuditEnable)(table, this.optsWithGlobals());
257
- });
311
+ })
312
+ .addHelpText("after", `
313
+ Notes:
314
+ - 开启审计会占用额外存储空间,计入应用的存储配额。
315
+ - 用 \`miaoda db quota\` 查看用量;用 \`miaoda db audit disable\` 或调整 --retention 控制开销。
316
+
317
+ Examples:
318
+ # 默认保留 7 天
319
+ $ miaoda db audit enable orders
320
+ ✓ Audit enabled for table 'orders' (retention: 7d)
321
+
322
+ # 指定保留时长 / 永久保留
323
+ $ miaoda db audit enable orders --retention 180d
324
+ $ miaoda db audit enable orders --retention forever
325
+
326
+ # JSON
327
+ $ miaoda db audit enable orders --retention 180d --json
328
+
329
+ # 报错:已经开启
330
+ $ miaoda db audit enable orders
331
+ Error: Audit is already enabled for table 'orders'
332
+ hint: Use \`miaoda db audit status orders\` to check current retention,
333
+ or run this command with a different --retention to update.
334
+
335
+ # 报错:retention 值非法
336
+ $ miaoda db audit enable orders --retention 60d
337
+ Error: Invalid retention '60d'
338
+ hint: Allowed values: 7d, 30d, 180d, 360d, forever.
339
+ `);
258
340
  auditCmd
259
341
  .command("disable")
260
342
  .summary("关闭表审计")
343
+ .description("关闭指定表的审计。")
261
344
  .usage("<table> [flags]")
262
345
  .argument("<table>", "目标表名")
263
346
  .action(async function (table) {
264
347
  await (0, index_1.handleDbAuditDisable)(table, this.optsWithGlobals());
265
- });
348
+ })
349
+ .addHelpText("after", `
350
+ Examples:
351
+ $ miaoda db audit disable orders
352
+ ✓ Audit disabled for table 'orders'
353
+
354
+ # JSON
355
+ $ miaoda db audit disable orders --json
356
+
357
+ # 报错:未开启
358
+ $ miaoda db audit disable orders
359
+ Error: Audit is not enabled for table 'orders'
360
+ hint: Use \`miaoda db audit status\` to see which tables have audit enabled.
361
+ `);
266
362
  auditCmd
267
363
  .command("list")
268
364
  .summary("查询审计日志")
269
- .description("按表 + 时间窗查询审计日志(INSERT / UPDATE / DELETE)。")
365
+ .description("查询一个或多个表的审计日志记录。多表查询时输出会带 target_table 列标识每条记录所属的表。")
270
366
  .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")
367
+ .argument("<tables...>", "一个或多个表名")
368
+ .option("--since <time>", "起始时间,支持 YYYY-MM-DD / ISO 8601 / 相对时间(同 changelog --since)")
369
+ .option("--until <time>", "截止时间,格式同 --since")
370
+ .option("--limit <n>", "返回条数上限(默认 20)", parsePositiveInt, 20)
371
+ .option("--cursor <token>", "从上一页返回的游标位置继续获取")
372
+ .option("--all", "获取全部结果,自动翻页")
276
373
  .action(async function (tables) {
277
374
  await (0, index_1.handleDbAuditList)(tables, this.optsWithGlobals());
278
- });
375
+ })
376
+ .addHelpText("after", `
377
+ Notes:
378
+ - 默认输出展示变更摘要(summary),完整变更快照(details 含 before / after)通过 --json 获取。
379
+ - 多表查询时不可用的表(未开启审计或不存在)会被跳过而非整体失败,全部不可用才返回错误。
380
+ - 退出码:只要有任何一张表有结果即为 0(跳过不算失败);全部不可用才为 1。
381
+
382
+ Examples:
383
+ # 单表
384
+ $ miaoda db audit list users --limit 5
385
+
386
+ # 多表(输出带 target_table 列区分来源)
387
+ $ miaoda db audit list users orders --limit 5
388
+
389
+ # 按时间范围
390
+ $ miaoda db audit list users --since 2026-04-14 --limit 10
391
+
392
+ # JSON(含完整 details / before / after)
393
+ $ miaoda db audit list users --limit 1 --json
394
+
395
+ # 报错:单表未开启
396
+ $ miaoda db audit list orders
397
+ Error: Audit is not enabled for table 'orders'
398
+ hint: Run \`miaoda db audit enable orders\` to start recording changes.
399
+
400
+ # 多表全部不可用才整体报错
401
+ $ miaoda db audit list orders invoices
402
+ Error: No audit data available for any of the specified tables
403
+ hint: Check audit status with \`miaoda db audit status\`.
404
+ `);
279
405
  // ── migration ──
280
406
  const migrationCmd = dbCmd
281
407
  .command("migration")
@@ -287,28 +413,106 @@ Examples:
287
413
  });
288
414
  migrationCmd
289
415
  .command("init")
290
- .summary("初始化多环境(不可逆)")
416
+ .summary("初始化多环境模式(dev / online)")
417
+ .description("初始化多环境模式(dev / online)。初始化后,dev 环境的数据库结构变更需要经 `migration apply` " +
418
+ "应用到 online;online 将无法直接更改数据库结构(仍可进行数据 DML 操作)。")
291
419
  .usage("[flags]")
292
- .option("--sync-data", "同时把现有数据复制到 dev")
293
- .option("--yes", "跳过 TTY 确认")
420
+ .option("--sync-data", "启用时将现有数据同步一份到 dev 环境")
421
+ .option("-y, --yes", "跳过确认提示直接执行")
294
422
  .action(async function () {
295
423
  await (0, index_1.handleDbMigrationInit)(this.optsWithGlobals());
296
- });
424
+ })
425
+ .addHelpText("after", `
426
+ Notes:
427
+ - 多环境模式一旦启用无法恢复为单库模式,请确认后再启用。
428
+
429
+ Examples:
430
+ # TTY 下会要求确认
431
+ $ miaoda db migration init
432
+ ? This action is irreversible. Initialize multi-env (dev / online)? (y/N) y
433
+ ✓ Multi-env initialized (dev / online)
434
+
435
+ # --yes 跳过确认(Agent / CI 场景)
436
+ $ miaoda db migration init --sync-data --yes
437
+ ✓ Multi-env initialized, data synced to dev
438
+
439
+ # JSON
440
+ $ miaoda db migration init --yes --json
441
+ {"data": {"status": "initialized", "environments": ["dev", "online"], "data_synced": false}}
442
+
443
+ # 报错:已经初始化过
444
+ $ miaoda db migration init
445
+ Error: Multi-env is already initialized
446
+ hint: Run \`miaoda db migration diff\` to view pending changes.
447
+ `);
297
448
  migrationCmd
298
449
  .command("diff")
299
450
  .summary("预览 dev → online 待发布变更")
451
+ .description("预览 dev → online 的待发布变更。只读预览,不会实际发布。")
300
452
  .usage("[flags]")
301
453
  .action(async function () {
302
454
  await (0, index_1.handleDbMigrationDiff)(this.optsWithGlobals());
303
- });
455
+ })
456
+ .addHelpText("after", `
457
+ Examples:
458
+ $ miaoda db migration diff
459
+ dev → online (2 changes):
460
+
461
+ ALTER TABLE users ADD COLUMN avatar_url text;
462
+ CREATE INDEX idx_users_avatar ON users(avatar_url);
463
+
464
+ # JSON
465
+ $ miaoda db migration diff --json
466
+
467
+ # 报错:无待发布变更
468
+ $ miaoda db migration diff
469
+ Error: No pending changes between dev and online
470
+ hint: Make schema changes in dev first (e.g. \`miaoda db sql "ALTER TABLE ..." --env dev\`).
471
+
472
+ # 报错:多环境未初始化
473
+ $ miaoda db migration diff
474
+ Error: Multi-env is not initialized
475
+ hint: Run \`miaoda db migration init\` to set up multi-env first.
476
+ `);
304
477
  migrationCmd
305
478
  .command("apply")
306
- .summary("应用 dev 变更到 online(多 DDL 单事务原子)")
479
+ .summary(" dev 的变更发布到 online(单事务原子)")
480
+ .description("将 dev 的变更发布到 online。")
307
481
  .usage("[flags]")
308
- .option("--yes", "跳过 TTY 二次确认")
482
+ .option("-y, --yes", "跳过确认提示直接执行")
309
483
  .action(async function () {
310
484
  await (0, index_1.handleDbMigrationApply)(this.optsWithGlobals());
311
- });
485
+ })
486
+ .addHelpText("after", `
487
+ Notes:
488
+ - 该操作会直接改动线上数据库,建议先用 \`migration diff\` 确认变更。
489
+ - 变更以单事务执行:多条 DDL 任何一条失败整批回滚,online 保持原状态。
490
+ - 退出码:0 表示全部成功;1 表示已回滚。
491
+
492
+ Examples:
493
+ # TTY 下会要求确认
494
+ $ miaoda db migration apply
495
+ ? Apply 2 changes to online? (y/N) y
496
+ ✓ Applied dev → online (2 changes)
497
+
498
+ # --yes 跳过确认
499
+ $ miaoda db migration apply --yes
500
+ ✓ Applied dev → online (2 changes)
501
+
502
+ # JSON
503
+ $ miaoda db migration apply --yes --json
504
+ {"data": {"status": "applied", "from": "dev", "to": "online", "changes_applied": 2}}
505
+
506
+ # 报错:无待发布变更
507
+ $ miaoda db migration apply
508
+ Error: No pending changes to apply
509
+ hint: Make schema changes in dev first, then run \`miaoda db migration diff\` to preview.
510
+
511
+ # 报错:多环境未启用
512
+ $ miaoda db migration apply
513
+ Error: Multi-env is not initialized
514
+ hint: Run \`miaoda db migration init\` to set up multi-env first.
515
+ `);
312
516
  // ── recovery(PITR)──
313
517
  const recoveryCmd = dbCmd
314
518
  .command("recovery")
@@ -321,26 +525,119 @@ Examples:
321
525
  recoveryCmd
322
526
  .command("diff")
323
527
  .summary("预览恢复到指定时间点的影响范围")
528
+ .description("预览恢复到指定时间点的影响范围(受影响的表、数据变更量、预计耗时)。只读预览,不改动数据。")
324
529
  .usage("<timestamp> [flags]")
325
- .argument("<timestamp>", "目标时间,YYYY-MM-DD / YYYY-MM-DD HH:MM[:SS] / ISO 8601")
530
+ .argument("<timestamp>", "目标时间,YYYY-MM-DD ISO 8601(如 2026-04-15T10:00:00Z)")
326
531
  .action(async function (target) {
327
532
  await (0, index_1.handleDbRecoveryDiff)(target, this.optsWithGlobals());
328
- });
533
+ })
534
+ .addHelpText("after", `
535
+ Notes:
536
+ - 超出可恢复窗口的时间点会报错,错误信息中会标注当前可恢复窗口。
537
+
538
+ Examples:
539
+ # TTY pretty
540
+ $ miaoda db recovery diff 2026-04-15T10:00:00Z
541
+ Recovery preview (→ 2026-04-15T10:00:00Z):
542
+
543
+ tables affected: 2
544
+ users: +3 rows, -1 row, ~5 rows modified
545
+ orders: table will be restored (was dropped at 10:25:00)
546
+
547
+ estimated time: ~30s
548
+
549
+ # non-TTY(管道,TAB 分列)
550
+ $ miaoda db recovery diff 2026-04-15T10:00:00Z | cat
551
+ Recovery preview (-> 2026-04-15T10:00:00Z):
552
+ tables_affected 2
553
+ users +3 rows, -1 row, ~5 rows modified
554
+ orders table will be restored (was dropped at 10:25:00)
555
+ estimated_time 30
556
+
557
+ # JSON
558
+ $ miaoda db recovery diff 2026-04-15T10:00:00Z --json
559
+
560
+ # 无变更(目标时间点与当前状态一致)
561
+ $ miaoda db recovery diff 2026-04-15T14:00:00Z
562
+ Recovery preview (→ 2026-04-15T14:00:00Z):
563
+
564
+ No changes — database is already at this state.
565
+
566
+ # 超出可恢复窗口
567
+ $ miaoda db recovery diff 2026-04-05T10:00:00Z
568
+ Error: Timestamp is outside the recoverable window
569
+ hint: Current recoverable window: 2026-04-09T12:00:00Z ~ 2026-04-16T12:00:00Z
570
+
571
+ # 时间格式错误
572
+ $ miaoda db recovery diff 2026/04/15
573
+ Error: Invalid timestamp format '2026/04/15'
574
+ hint: Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z
575
+ `);
329
576
  recoveryCmd
330
577
  .command("apply")
331
- .summary("触发恢复到指定时间点(不可逆覆盖)")
578
+ .summary("将数据库恢复到指定时间点的状态")
579
+ .description("将数据库恢复到指定时间点的状态。")
332
580
  .usage("<timestamp> [flags]")
333
- .argument("<timestamp>", "目标时间,YYYY-MM-DD / YYYY-MM-DD HH:MM[:SS] / ISO 8601")
334
- .option("--yes", "跳过 TTY 二次确认")
581
+ .argument("<timestamp>", "目标时间,YYYY-MM-DD ISO 8601(如 2026-04-15T10:00:00Z)")
582
+ .option("-y, --yes", "跳过确认提示直接执行")
335
583
  .action(async function (target) {
336
584
  await (0, index_1.handleDbRecoveryApply)(target, this.optsWithGlobals());
337
- });
585
+ })
586
+ .addHelpText("after", `
587
+ Notes:
588
+ - 该操作会覆盖当前数据且不可撤销,建议先用 \`recovery diff\` 预览影响。
589
+ - 恢复过程是原子的:中途失败时数据库回到操作前的状态,不会出现部分恢复。
590
+ - 退出码:0 表示完成;1 表示已回滚。
591
+
592
+ Examples:
593
+ # TTY 下会要求确认
594
+ $ miaoda db recovery apply 2026-04-15T10:00:00Z
595
+ ? Restore database to 2026-04-15T10:00:00Z? This will overwrite current data. (y/N) y
596
+ ✓ Database restored to 2026-04-15T10:00:00Z (2 tables affected, 30s elapsed)
597
+
598
+ # --yes 跳过确认
599
+ $ miaoda db recovery apply 2026-04-15T10:00:00Z --yes
600
+
601
+ # JSON
602
+ $ miaoda db recovery apply 2026-04-15T10:00:00Z --yes --json
603
+
604
+ # 超出可恢复窗口
605
+ $ miaoda db recovery apply 2026-04-05T10:00:00Z
606
+ Error: Timestamp is outside the recoverable window
607
+ hint: Current recoverable window: 2026-04-09T12:00:00Z ~ 2026-04-16T12:00:00Z
608
+ Run \`miaoda db recovery diff <ts>\` within this window to preview impact.
609
+
610
+ # 并发冲突(已有恢复任务在执行)
611
+ $ miaoda db recovery apply 2026-04-15T10:00:00Z
612
+ Error: Another recovery is already in progress
613
+ hint: Wait for the current recovery to finish.
614
+ `);
338
615
  // ── quota ──
339
616
  dbCmd
340
617
  .command("quota")
341
- .summary("查看数据库用量与限额")
618
+ .summary("查看数据库的存储用量与限额")
619
+ .description("查看数据库的存储用量、配额和表数量。")
342
620
  .usage("[flags]")
343
621
  .action(async function () {
344
622
  await (0, index_1.handleDbQuota)(this.optsWithGlobals());
345
- });
623
+ })
624
+ .addHelpText("after", `
625
+ Examples:
626
+ $ miaoda db quota
627
+ Storage: 14.9 MB / 1 GB (1.5%)
628
+ Tables: 3
629
+ Views: 10
630
+
631
+ # JSON
632
+ $ miaoda db quota --json
633
+ {
634
+ "data": {
635
+ "storage_used_bytes": 15623782,
636
+ "storage_quota_bytes": 1073741824,
637
+ "usage_percent": 1.5,
638
+ "tables": 3,
639
+ "views": 10
640
+ }
641
+ }
642
+ `);
346
643
  }