@lark-apaas/miaoda-cli 0.1.2 → 0.1.3-alpha.40be425

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 (70) hide show
  1. package/README.md +8 -7
  2. package/dist/api/app/api.js +25 -0
  3. package/dist/api/app/index.js +15 -0
  4. package/dist/api/app/schemas.js +79 -0
  5. package/dist/api/app/types.js +58 -0
  6. package/dist/api/db/api.js +331 -24
  7. package/dist/api/db/client.js +36 -0
  8. package/dist/api/db/index.js +11 -1
  9. package/dist/api/deploy/api.js +60 -0
  10. package/dist/api/deploy/index.js +16 -0
  11. package/dist/api/deploy/schemas.js +105 -0
  12. package/dist/api/deploy/types.js +22 -0
  13. package/dist/api/file/api.js +25 -23
  14. package/dist/api/file/index.js +2 -1
  15. package/dist/api/file/parsers.js +14 -3
  16. package/dist/api/index.js +7 -1
  17. package/dist/api/observability/api.js +52 -0
  18. package/dist/api/observability/index.js +16 -0
  19. package/dist/api/observability/schemas.js +60 -0
  20. package/dist/api/observability/types.js +27 -0
  21. package/dist/cli/commands/app/index.js +62 -0
  22. package/dist/cli/commands/db/index.js +577 -5
  23. package/dist/cli/commands/deploy/index.js +155 -0
  24. package/dist/cli/commands/file/index.js +22 -7
  25. package/dist/cli/commands/index.js +10 -6
  26. package/dist/cli/commands/observability/index.js +240 -0
  27. package/dist/cli/commands/shared.js +83 -7
  28. package/dist/cli/handlers/app/get.js +47 -0
  29. package/dist/cli/handlers/app/index.js +7 -0
  30. package/dist/cli/handlers/app/update.js +59 -0
  31. package/dist/cli/handlers/db/audit.js +294 -0
  32. package/dist/cli/handlers/db/changelog.js +130 -0
  33. package/dist/cli/handlers/db/data.js +6 -6
  34. package/dist/cli/handlers/db/index.js +17 -1
  35. package/dist/cli/handlers/db/migration.js +235 -0
  36. package/dist/cli/handlers/db/quota.js +68 -0
  37. package/dist/cli/handlers/db/recovery.js +357 -0
  38. package/dist/cli/handlers/db/schema.js +2 -3
  39. package/dist/cli/handlers/db/sql.js +2 -3
  40. package/dist/cli/handlers/deploy/deploy.js +84 -0
  41. package/dist/cli/handlers/deploy/error-log.js +60 -0
  42. package/dist/cli/handlers/deploy/format.js +39 -0
  43. package/dist/cli/handlers/deploy/get.js +71 -0
  44. package/dist/cli/handlers/deploy/helpers.js +41 -0
  45. package/dist/cli/handlers/deploy/history.js +70 -0
  46. package/dist/cli/handlers/deploy/index.js +14 -0
  47. package/dist/cli/handlers/deploy/polling.js +162 -0
  48. package/dist/cli/handlers/file/cp.js +1 -2
  49. package/dist/cli/handlers/file/index.js +3 -1
  50. package/dist/cli/handlers/file/ls.js +1 -2
  51. package/dist/cli/handlers/file/quota.js +66 -0
  52. package/dist/cli/handlers/file/rm.js +1 -2
  53. package/dist/cli/handlers/file/sign.js +1 -2
  54. package/dist/cli/handlers/file/stat.js +3 -3
  55. package/dist/cli/handlers/observability/analytics.js +212 -0
  56. package/dist/cli/handlers/observability/helpers.js +66 -0
  57. package/dist/cli/handlers/observability/index.js +12 -0
  58. package/dist/cli/handlers/observability/log.js +94 -0
  59. package/dist/cli/handlers/observability/metric.js +208 -0
  60. package/dist/cli/handlers/observability/trace.js +102 -0
  61. package/dist/main.js +6 -2
  62. package/dist/utils/args.js +8 -0
  63. package/dist/utils/devops-error.js +28 -0
  64. package/dist/utils/git.js +29 -0
  65. package/dist/utils/http.js +118 -0
  66. package/dist/utils/index.js +15 -1
  67. package/dist/utils/output.js +360 -7
  68. package/dist/utils/poll.js +27 -0
  69. package/dist/utils/time.js +208 -0
  70. package/package.json +7 -5
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.handleDbMigrationInit = handleDbMigrationInit;
40
+ exports.handleDbMigrationDiff = handleDbMigrationDiff;
41
+ exports.handleDbMigrationApply = handleDbMigrationApply;
42
+ const node_readline_1 = __importDefault(require("node:readline"));
43
+ const api = __importStar(require("../../../api/index"));
44
+ const shared_1 = require("../../../cli/commands/shared");
45
+ const colors_1 = require("../../../utils/colors");
46
+ const error_1 = require("../../../utils/error");
47
+ const output_1 = require("../../../utils/output");
48
+ const poll_1 = require("../../../utils/poll");
49
+ const render_1 = require("../../../utils/render");
50
+ async function handleDbMigrationInit(opts) {
51
+ const appId = (0, shared_1.resolveAppId)(opts);
52
+ // 不可逆操作,TTY 默认要求 y/N;--yes 跳过
53
+ if (!opts.yes && !(0, output_1.isJsonMode)()) {
54
+ // 文案对齐 PRD:先讲不可逆,再问是否初始化;syncData 时追加一句而不是塞进同一句话
55
+ const suffix = opts.syncData ? " (existing data will be copied to dev)" : "";
56
+ const ok = await confirm(`? This action is irreversible. Initialize multi-env (dev / online)${suffix}? (y/N) `);
57
+ if (!ok) {
58
+ (0, output_1.emit)("Aborted.");
59
+ return;
60
+ }
61
+ }
62
+ let result;
63
+ try {
64
+ result = await api.db.migrationInit({ appId, syncData: opts.syncData });
65
+ }
66
+ catch (err) {
67
+ // PRD: 重复 init 报错带 hint,引导用户去 diff 看待发布变更
68
+ if (err instanceof error_1.AppError && err.code === "DB_API_k_dl_1300034") {
69
+ throw new error_1.AppError(err.code, err.message, {
70
+ next_actions: ["Run `miaoda db migration diff` to view pending changes."],
71
+ });
72
+ }
73
+ throw err;
74
+ }
75
+ if ((0, output_1.isJsonMode)()) {
76
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)(result));
77
+ return;
78
+ }
79
+ // PRD 单行格式(不用 key:value 表格):
80
+ // 默认 ✓ / OK: "Multi-env initialized (dev / online)"
81
+ // --sync-data: "Multi-env initialized, data synced to dev"
82
+ const tty = (0, render_1.isStdoutTty)();
83
+ const body = result.dataSynced
84
+ ? "Multi-env initialized, data synced to dev"
85
+ : `Multi-env initialized (${result.environments.join(" / ")})`;
86
+ (0, output_1.emit)(tty ? colors_1.c.success(`✓ ${body}`) : `OK ${body}`);
87
+ }
88
+ async function handleDbMigrationDiff(opts) {
89
+ const appId = (0, shared_1.resolveAppId)(opts);
90
+ let result;
91
+ try {
92
+ result = await api.db.migrate({ appId, dryRun: true });
93
+ }
94
+ catch (err) {
95
+ throw decorateMigrationError(err);
96
+ }
97
+ // PRD: diff 在无待发布变更时报错带 hint,而不是渲染空列表
98
+ if (result.changes.length === 0) {
99
+ throw new error_1.AppError("DB_API_k_dl_1300035", `No pending changes between ${result.from} and ${result.to}`, {
100
+ next_actions: [
101
+ 'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
102
+ ],
103
+ });
104
+ }
105
+ renderDiff(result);
106
+ }
107
+ async function handleDbMigrationApply(opts) {
108
+ const appId = (0, shared_1.resolveAppId)(opts);
109
+ // TTY 下先 diff 给用户审;--yes 直接打到 online
110
+ if (!opts.yes && !(0, output_1.isJsonMode)()) {
111
+ let preview;
112
+ try {
113
+ preview = await api.db.migrate({ appId, dryRun: true });
114
+ }
115
+ catch (err) {
116
+ throw decorateMigrationError(err);
117
+ }
118
+ if (preview.changes.length === 0) {
119
+ // PRD 文案 + hint
120
+ throw new error_1.AppError("DB_API_k_dl_1300035", `No pending changes between ${preview.from} and ${preview.to}`, {
121
+ next_actions: [
122
+ 'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
123
+ ],
124
+ });
125
+ }
126
+ renderDiff(preview);
127
+ const ok = await confirm(`? Apply ${String(preview.changes.length)} change(s) to ${preview.to}? (y/N) `);
128
+ if (!ok) {
129
+ (0, output_1.emit)("Aborted.");
130
+ return;
131
+ }
132
+ }
133
+ let result;
134
+ try {
135
+ result = await api.db.migrate({ appId, dryRun: false });
136
+ }
137
+ catch (err) {
138
+ throw decorateMigrationError(err);
139
+ }
140
+ // dataloom 立即返 taskId(apply 实际是异步流水线)。CLI 自己 poll 直到 success/failed,
141
+ // 避免单次 HTTP 长连接 30s+ 被网关 / SDK 中断。
142
+ if (result.taskId === undefined || result.taskId === "") {
143
+ throw new error_1.AppError("INTERNAL_DB_ERROR", "migration apply did not return taskId");
144
+ }
145
+ const taskId = result.taskId;
146
+ const final = await (0, poll_1.pollUntilDone)({
147
+ label: "migration apply",
148
+ intervalMs: 1000,
149
+ fetch: () => api.db.getMigrationStatus({ appId, taskId }),
150
+ isDone: (cur) => {
151
+ // 同 recovery preview,dataloom 上下游枚举大小写不完全统一,客户端归一兜底。
152
+ const status = cur.status.toLowerCase();
153
+ if (status === "success")
154
+ return { done: true, value: cur };
155
+ if (status === "failed") {
156
+ throw new error_1.AppError("DB_API_k_dl_1300030", cur.errorMessage ?? `migration apply failed (taskId=${taskId})`);
157
+ }
158
+ return { done: false };
159
+ },
160
+ });
161
+ const appliedCount = final.changesApplied ?? result.changes.length;
162
+ if ((0, output_1.isJsonMode)()) {
163
+ // PRD:{"status": "applied", "from": "dev", "to": "online", "changes_applied": 2}
164
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
165
+ status: "applied",
166
+ from: result.from,
167
+ to: result.to,
168
+ changesApplied: appliedCount,
169
+ }));
170
+ return;
171
+ }
172
+ const tty = (0, render_1.isStdoutTty)();
173
+ const arrow = tty ? "→" : "->";
174
+ const body = `Applied ${result.from} ${arrow} ${result.to} (${String(appliedCount)} changes)`;
175
+ (0, output_1.emit)(tty ? colors_1.c.success(`✓ ${body}`) : `OK ${body}`);
176
+ }
177
+ // ── helpers ──
178
+ // PRD diff 输出:
179
+ // dev → online (2 changes):
180
+ //
181
+ // ALTER TABLE users ADD COLUMN avatar_url text;
182
+ // CREATE INDEX idx_users_avatar ON users(avatar_url);
183
+ function renderDiff(result) {
184
+ if ((0, output_1.isJsonMode)()) {
185
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
186
+ from: result.from,
187
+ to: result.to,
188
+ changes: result.changes.map((c) => ({
189
+ type: c.type,
190
+ table: c.table,
191
+ statement: c.statement,
192
+ })),
193
+ }));
194
+ return;
195
+ }
196
+ const tty = (0, render_1.isStdoutTty)();
197
+ if (result.changes.length === 0) {
198
+ (0, output_1.emit)(`No pending changes from ${result.from} to ${result.to}.`);
199
+ return;
200
+ }
201
+ const arrow = tty ? "→" : "->";
202
+ (0, output_1.emit)(`${result.from} ${arrow} ${result.to} (${String(result.changes.length)} changes):\n\n` +
203
+ result.changes.map((c) => ` ${c.statement}`).join("\n"));
204
+ }
205
+ // decorateMigrationError 给 migration / recovery 路径上的几个错误码补 PRD 规定的 hint。
206
+ // dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
207
+ function decorateMigrationError(err) {
208
+ if (!(err instanceof error_1.AppError))
209
+ return err;
210
+ switch (err.code) {
211
+ case "DB_API_k_dl_1300039":
212
+ // 多环境未初始化:引导先 init
213
+ return new error_1.AppError(err.code, err.message, {
214
+ next_actions: ["Run `miaoda db migration init` to set up multi-env first."],
215
+ });
216
+ case "DB_API_k_dl_1300035":
217
+ // 无待发布变更:引导先在 dev 改 schema
218
+ return new error_1.AppError(err.code, err.message, {
219
+ next_actions: [
220
+ 'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
221
+ ],
222
+ });
223
+ default:
224
+ return err;
225
+ }
226
+ }
227
+ async function confirm(prompt) {
228
+ const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
229
+ return new Promise((resolve) => {
230
+ rl.question(prompt, (answer) => {
231
+ rl.close();
232
+ resolve(answer.trim().toLowerCase() === "y");
233
+ });
234
+ });
235
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.handleDbQuota = handleDbQuota;
37
+ const api = __importStar(require("../../../api/index"));
38
+ const shared_1 = require("../../../cli/commands/shared");
39
+ const output_1 = require("../../../utils/output");
40
+ const render_1 = require("../../../utils/render");
41
+ async function handleDbQuota(opts) {
42
+ const appId = (0, shared_1.resolveAppId)(opts);
43
+ const data = await api.db.getDbQuota({ appId, dbBranch: opts.env });
44
+ if ((0, output_1.isJsonMode)()) {
45
+ // 配额未对接(storageQuotaBytes=0)时,quota / usage_percent 字段都不输出
46
+ const out = {
47
+ storageUsedBytes: data.storageUsedBytes,
48
+ tables: data.tables,
49
+ views: data.views,
50
+ };
51
+ if (data.storageQuotaBytes > 0) {
52
+ out.storageQuotaBytes = data.storageQuotaBytes;
53
+ out.usagePercent = data.usagePercent;
54
+ }
55
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)(out));
56
+ return;
57
+ }
58
+ // PRD:单行 "Storage: 14.9 MB / 1 GB (1.5%)";配额未对接时只显示 used
59
+ const tty = (0, render_1.isStdoutTty)();
60
+ const storageLine = data.storageQuotaBytes > 0
61
+ ? `${(0, render_1.formatSize)(data.storageUsedBytes)} / ${(0, render_1.formatSize)(data.storageQuotaBytes)} (${data.usagePercent.toFixed(1)}%)`
62
+ : (0, render_1.formatSize)(data.storageUsedBytes);
63
+ (0, output_1.emit)((0, render_1.renderKeyValue)([
64
+ ["Storage", storageLine],
65
+ ["Tables", String(data.tables)],
66
+ ["Views", String(data.views)],
67
+ ], tty));
68
+ }
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.handleDbRecoveryDiff = handleDbRecoveryDiff;
40
+ exports.handleDbRecoveryApply = handleDbRecoveryApply;
41
+ const node_readline_1 = __importDefault(require("node:readline"));
42
+ const api = __importStar(require("../../../api/index"));
43
+ const shared_1 = require("../../../cli/commands/shared");
44
+ const colors_1 = require("../../../utils/colors");
45
+ const error_1 = require("../../../utils/error");
46
+ const output_1 = require("../../../utils/output");
47
+ const poll_1 = require("../../../utils/poll");
48
+ const render_1 = require("../../../utils/render");
49
+ const time_1 = require("../../../utils/time");
50
+ // ── recovery diff ──
51
+ //
52
+ // PITR preview 是异步任务:dataloom 立即返 previewRequestId,CLI 自己 poll 直到 success/failed。
53
+ // 失败时 dataloom 已用 translateRestoreErr 把中文窗口错误翻成 PRD 英文 + k_dl_1300036 code,
54
+ // CLI 走 decorateRecoveryError 补 hint。
55
+ async function handleDbRecoveryDiff(target, opts) {
56
+ const appId = (0, shared_1.resolveAppId)(opts);
57
+ const ts = normalizeTimestamp(target);
58
+ const preview = await runRecoveryPreview(appId, ts);
59
+ renderDiff(ts, preview);
60
+ }
61
+ async function handleDbRecoveryApply(target, opts) {
62
+ const appId = (0, shared_1.resolveAppId)(opts);
63
+ const ts = normalizeTimestamp(target);
64
+ // PRD 要求 apply 输出包含 N tables affected / Ms elapsed,dataloom apply 路径不
65
+ // 回带这俩信息(pgsvc.BranchRestore 是异步触发、不等终态)。所以 CLI 这里始终
66
+ // 先跑一次 preview——它本来就是 PITR 推荐流程,--yes 时静默跑、TTY 时同时给用户审。
67
+ const preview = await runRecoveryPreview(appId, ts);
68
+ const tablesAffected = preview.tablesAffected ?? preview.changes?.length ?? 0;
69
+ // dataloom 端 apply 是异步触发,elapsed_seconds 返 0。这里改用 preview 给出的
70
+ // estimated_seconds 当 "预计耗时",更贴近 PRD 示例的 30s elapsed 含义;
71
+ // 都没填时按 PRD 默认值 30 兜底。
72
+ const elapsedFromPreview = preview.estimatedSeconds ?? 30;
73
+ if (!opts.yes && !(0, output_1.isJsonMode)()) {
74
+ renderDiff(ts, preview);
75
+ const ok = await confirm(`? Restore database to ${ts}? This will overwrite current data. (y/N) `);
76
+ if (!ok) {
77
+ (0, output_1.emit)("Aborted.");
78
+ return;
79
+ }
80
+ }
81
+ let result;
82
+ try {
83
+ result = await api.db.recover({ appId, target: ts, dryRun: false });
84
+ }
85
+ catch (err) {
86
+ throw decorateRecoveryError(err);
87
+ }
88
+ const elapsed = result.elapsedSeconds && result.elapsedSeconds > 0 ? result.elapsedSeconds : elapsedFromPreview;
89
+ if ((0, output_1.isJsonMode)()) {
90
+ // PRD:{"status": "restored", "target": "...", "tables_affected": N, "elapsed_seconds": M}
91
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
92
+ status: "restored",
93
+ target: result.target,
94
+ tablesAffected,
95
+ elapsedSeconds: elapsed,
96
+ }));
97
+ return;
98
+ }
99
+ const tty = (0, render_1.isStdoutTty)();
100
+ const body = `Database restored to ${result.target} ` +
101
+ `(${String(tablesAffected)} tables affected, ${String(elapsed)}s elapsed)`;
102
+ (0, output_1.emit)(tty ? colors_1.c.success(`✓ ${body}`) : `OK ${body}`);
103
+ }
104
+ /**
105
+ * 触发 PITR 预览任务并轮询到终态:
106
+ * 1. POST /db/recovery dryRun=true → previewRequestId
107
+ * 2. GET /db/recovery/preview?previewRequestId 直到 previewStatus=success/failed
108
+ *
109
+ * 错误透传:业务错误(窗口超限 / 格式错)由 dataloom 直接 throw,CLI 在 catch 里
110
+ * 补 hint。failed 状态下 dataloom 已把 errorMessage 翻好,CLI 包成 k_dl_1300036
111
+ * 让 decorateRecoveryError 命中窗口超限 hint。
112
+ */
113
+ async function runRecoveryPreview(appId, ts) {
114
+ let triggered;
115
+ try {
116
+ triggered = await api.db.recover({ appId, target: ts, dryRun: true });
117
+ }
118
+ catch (err) {
119
+ throw decorateRecoveryError(err);
120
+ }
121
+ if (triggered.previewRequestId === undefined || triggered.previewRequestId === "") {
122
+ throw new error_1.AppError("INTERNAL_DB_ERROR", "recovery diff did not return previewRequestId");
123
+ }
124
+ const previewRequestId = triggered.previewRequestId;
125
+ try {
126
+ return await (0, poll_1.pollUntilDone)({
127
+ label: "recovery preview",
128
+ intervalMs: 1000,
129
+ fetch: () => api.db.getRecoveryPreview({ appId, previewRequestId }),
130
+ isDone: (cur) => {
131
+ // dataloom 内部 pgsvc 返回首字母大写枚举(Pending/Running/Success/Failed),
132
+ // 但 admin-inner thrift 契约里写的是小写。客户端做大小写归一兜底,避免依赖
133
+ // 上游 case 一致性导致死循环。
134
+ const status = cur.previewStatus.toLowerCase();
135
+ if (status === "success")
136
+ return { done: true, value: cur };
137
+ if (status === "failed") {
138
+ // 复用 k_dl_1300036(窗口超限 / 预览失败的统一对外码),让 decorateRecoveryError
139
+ // 命中后给 PITR 7 天窗口的 hint。errorMessage 优先用 dataloom 翻好的 PRD 文案。
140
+ throw new error_1.AppError("DB_API_k_dl_1300036", cur.errorMessage ?? "recovery preview failed");
141
+ }
142
+ return { done: false };
143
+ },
144
+ });
145
+ }
146
+ catch (err) {
147
+ throw decorateRecoveryError(err);
148
+ }
149
+ }
150
+ // ── helpers ──
151
+ /**
152
+ * 把用户传入的时间统一成 ISO 8601 UTC 字符串发给 dataloom。
153
+ *
154
+ * 复用 utils/time.ts::parseTimeToMs(跟 file ls / db changelog / db audit list 同一套
155
+ * 解析器),支持四种形态:
156
+ * - 相对时间:30s / 5m / 2h / 3d / 1w(从 now 往前推)
157
+ * - 仅日期: 2026-04-15 → 本地时区当日 00:00:00
158
+ * - 本地 datetime: 2026-04-15T10:00:00 → 本地时区
159
+ * - ISO 8601 with TZ: 2026-04-15T10:00:00Z 或 2026-04-15T10:00:00+08:00
160
+ *
161
+ * 不接受松散 / 区域性格式(YYYY/MM/DD、'April 1 2026' 等)——parseTimeToMs 内部
162
+ * 用严格 regex 把这些拒掉,避免之前用 new Date(input) 兜底把脏值送到后端的坑。
163
+ */
164
+ function normalizeTimestamp(input) {
165
+ if (input === "") {
166
+ throw new error_1.AppError("ARGS_INVALID", "target timestamp is required", {
167
+ next_actions: ["Usage: miaoda db recovery diff|apply <timestamp>"],
168
+ });
169
+ }
170
+ return new Date((0, time_1.parseTimeToMs)(input)).toISOString();
171
+ }
172
+ // PRD diff 输出,三套:
173
+ //
174
+ // TTY pretty(缩进 prose、带 Unicode 箭头 / 波浪号、表名按列对齐):
175
+ // Recovery preview (→ 2026-04-15T10:00:00Z):
176
+ //
177
+ // tables affected: 2
178
+ // users: +3 rows, -1 row, ~5 rows modified
179
+ // orders: table will be restored (was dropped at 10:25:00)
180
+ //
181
+ // estimated time: ~30s
182
+ //
183
+ // non-TTY pretty(管道友好,扁平 TSV、ASCII 箭头、snake_case key、estimated_time 不带 ~/s):
184
+ // Recovery preview (-> 2026-04-15T10:00:00Z):
185
+ // tables_affected\t2
186
+ // users\t+3 rows, -1 row, ~5 rows modified
187
+ // orders\ttable will be restored (was dropped at 10:25:00)
188
+ // estimated_time\t30
189
+ //
190
+ // --json:标准 envelope,字段名固定 snake_case。
191
+ function renderDiff(target, preview) {
192
+ const changes = preview.changes ?? [];
193
+ const tablesAffected = preview.tablesAffected ?? changes.length;
194
+ const estimated = preview.estimatedSeconds;
195
+ if ((0, output_1.isJsonMode)()) {
196
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
197
+ target,
198
+ tablesAffected,
199
+ changes: changes.map((c) => ({
200
+ table: c.table,
201
+ inserted: c.inserted,
202
+ deleted: c.deleted,
203
+ modified: c.modified,
204
+ action: c.action,
205
+ droppedAt: c.droppedAt,
206
+ })),
207
+ estimatedSeconds: estimated ?? 30,
208
+ }));
209
+ return;
210
+ }
211
+ const tty = (0, render_1.isStdoutTty)();
212
+ if (tty) {
213
+ (0, output_1.emit)(renderDiffTty(target, changes, tablesAffected, estimated));
214
+ return;
215
+ }
216
+ (0, output_1.emit)(renderDiffPipe(target, changes, tablesAffected, estimated));
217
+ }
218
+ function renderDiffTty(target, changes, tablesAffected, estimated) {
219
+ const header = `Recovery preview (→ ${target}):`;
220
+ if (changes.length === 0) {
221
+ return `${header}\n\n No changes — database is already at this state.`;
222
+ }
223
+ // 对齐:所有表名右侧补到 max(len) + 1 个 ":" + 1 个空格再起描述。PRD 用例里
224
+ // "users: " / "orders: " 三/二空格其实就是按 6 个 char 列宽对齐出来的。
225
+ const maxName = Math.max(...changes.map((c) => c.table.length));
226
+ const lines = [header, "", ` tables affected: ${String(tablesAffected)}`];
227
+ for (const c of changes) {
228
+ const pad = " ".repeat(Math.max(1, maxName - c.table.length + 2));
229
+ lines.push(` ${c.table}:${pad}${describeChange(c)}`);
230
+ }
231
+ // PRD 要求 estimated time 块固定出现(即便 dataloom 当前没填 EstimatedSeconds,
232
+ // CLI 也用 30s 兜底——PRD 示例展示的就是 30s,统一一个不至于误导的常量)。
233
+ lines.push("");
234
+ lines.push(` estimated time: ~${String(estimated ?? 30)}s`);
235
+ return lines.join("\n");
236
+ }
237
+ function renderDiffPipe(target, changes, tablesAffected, estimated) {
238
+ // 管道场景:去掉缩进,TAB 分列,箭头降级为 ->,key 用 snake_case,
239
+ // estimated_time 只输出秒数(不带 ~/s),方便 awk/grep。
240
+ const header = `Recovery preview (-> ${target}):`;
241
+ if (changes.length === 0) {
242
+ return `${header}\nNo changes — database is already at this state.`;
243
+ }
244
+ const lines = [header, `tables_affected\t${String(tablesAffected)}`];
245
+ for (const c of changes) {
246
+ lines.push(`${c.table}\t${describeChange(c)}`);
247
+ }
248
+ // 与 TTY 一致,始终输出 estimated_time(默认 30),让 awk 解析时不用兜底。
249
+ lines.push(`estimated_time\t${String(estimated ?? 30)}`);
250
+ return lines.join("\n");
251
+ }
252
+ function describeChange(c) {
253
+ // 表级操作:dataloom 现在把 schema diff 透出为 action="schema_<diffType>",
254
+ // 老协议的 restore_table / drop / create 一并保留兼容。
255
+ if (c.action === "restore_table") {
256
+ const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
257
+ return `table will be restored${ts}`;
258
+ }
259
+ if (c.action === "drop") {
260
+ const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
261
+ return `table will be dropped${ts}`;
262
+ }
263
+ if (c.action === "create") {
264
+ const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
265
+ return `table will be created${ts}`;
266
+ }
267
+ if (c.action?.startsWith("schema_") === true) {
268
+ // dataloom 透出的 schema diffType 来自 schema_handler/common/constants:
269
+ // - create: 当前没这表 / 目标时间点有 → 恢复后表会被建出来(PRD 用 restored 表达)
270
+ // - drop: 当前有 / 目标时间点没 → 恢复后会被删掉
271
+ // - alter: 两侧都在但结构有差异 → 列 / 索引 / 关系等会被改回
272
+ const diffType = c.action.slice("schema_".length);
273
+ const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
274
+ switch (diffType) {
275
+ case "create":
276
+ return `table will be restored${ts}`;
277
+ case "drop":
278
+ return `table will be dropped${ts}`;
279
+ case "alter":
280
+ return `schema will be altered${ts}`;
281
+ default:
282
+ return `schema changed${diffType !== "" ? ` (${diffType})` : ""}${ts}`;
283
+ }
284
+ }
285
+ if (c.action === "unavailable") {
286
+ // dataloom 端 count 失败的表,复用 droppedAt 透传 message
287
+ return `diff unavailable${c.droppedAt !== undefined && c.droppedAt !== "" ? `: ${c.droppedAt}` : ""}`;
288
+ }
289
+ // 数据变更:+N rows / -N rows / ~N rows modified
290
+ const parts = [];
291
+ if (c.inserted !== undefined && c.inserted !== 0)
292
+ parts.push(`+${String(c.inserted)} rows`);
293
+ if (c.deleted !== undefined && c.deleted !== 0) {
294
+ parts.push(`-${String(c.deleted)} ${c.deleted === 1 ? "row" : "rows"}`);
295
+ }
296
+ if (c.modified !== undefined && c.modified !== 0)
297
+ parts.push(`~${String(c.modified)} rows modified`);
298
+ return parts.length === 0 ? "no changes" : parts.join(", ");
299
+ }
300
+ /**
301
+ * 解析 dataloom 嵌在 message 末尾的窗口边界,形如 `[2026-04-09T12:00:00Z ~ 2026-04-16T12:00:00Z]`。
302
+ * 命中返 `{start, end}`,否则 null。两侧时间字符串原样透传给 hint,不做归一化。
303
+ */
304
+ function parseWindowBounds(message) {
305
+ const m = /\[([^[\]]+?)\s*~\s*([^[\]]+?)\]\s*$/.exec(message);
306
+ if (!m)
307
+ return null;
308
+ const start = m[1].trim();
309
+ const end = m[2].trim();
310
+ if (start === "" || end === "")
311
+ return null;
312
+ return { start, end };
313
+ }
314
+ // decorateRecoveryError 给 recovery 路径上的几个错误码补 PRD 规定的 hint。
315
+ // dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
316
+ function decorateRecoveryError(err) {
317
+ if (!(err instanceof error_1.AppError))
318
+ return err;
319
+ switch (err.code) {
320
+ case "DB_API_k_dl_1300036": {
321
+ // 窗口超限:dataloom 端会把当前窗口边界拼成 `[<startISO> ~ <endISO>]` 嵌到 message
322
+ // 末尾,CLI 在这里提取出来按 PRD 格式 hint。提取失败时退回通用提示。
323
+ const bounds = parseWindowBounds(err.message);
324
+ if (bounds === null) {
325
+ return new error_1.AppError(err.code, err.message, {
326
+ next_actions: [
327
+ "PITR window is up to 7 days back, limited by your last `db migration apply` time.",
328
+ ],
329
+ });
330
+ }
331
+ // message 里把 [start ~ end] 段移除掉,避免重复
332
+ const cleanMsg = err.message.replace(/\s*\[[^\]]*\]\s*$/, "");
333
+ return new error_1.AppError(err.code, cleanMsg, {
334
+ next_actions: [
335
+ `Current recoverable window: ${bounds.start} ~ ${bounds.end}`,
336
+ `(limited by last migration apply at ${bounds.start})`,
337
+ ],
338
+ });
339
+ }
340
+ case "DB_API_k_dl_1300038":
341
+ // 时间格式错误:引导 ISO 8601
342
+ return new error_1.AppError(err.code, err.message, {
343
+ next_actions: ["Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z"],
344
+ });
345
+ default:
346
+ return err;
347
+ }
348
+ }
349
+ async function confirm(prompt) {
350
+ const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
351
+ return new Promise((resolve) => {
352
+ rl.question(prompt, (answer) => {
353
+ rl.close();
354
+ resolve(answer.trim().toLowerCase() === "y");
355
+ });
356
+ });
357
+ }
@@ -39,12 +39,11 @@ const api = __importStar(require("../../../api/index"));
39
39
  const error_1 = require("../../../utils/error");
40
40
  const output_1 = require("../../../utils/output");
41
41
  const render_1 = require("../../../utils/render");
42
- const shared_1 = require("../../../cli/commands/shared");
43
42
  const colors_1 = require("../../../utils/colors");
44
43
  const index_1 = require("../../../api/db/index");
45
44
  // ── schema list ──
46
45
  async function handleDbSchemaList(opts) {
47
- const appId = (0, shared_1.resolveAppId)(opts);
46
+ const appId = opts.appId;
48
47
  const resp = await api.db.getSchema({
49
48
  appId,
50
49
  format: "schema",
@@ -80,7 +79,7 @@ async function handleDbSchemaList(opts) {
80
79
  (0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
81
80
  }
82
81
  async function handleDbSchemaGet(table, opts) {
83
- const appId = (0, shared_1.resolveAppId)(opts);
82
+ const appId = opts.appId;
84
83
  const tty = (0, render_1.isStdoutTty)();
85
84
  const forceDdl = Boolean(opts.ddl);
86
85
  const wantsStructured = (0, output_1.isJsonMode)() || (tty && !forceDdl);