@lark-apaas/miaoda-cli 0.1.2 → 0.1.3-alpha.09899c4

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 (85) 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 +390 -55
  7. package/dist/api/db/client.js +65 -25
  8. package/dist/api/db/index.js +12 -1
  9. package/dist/api/db/parsers.js +20 -20
  10. package/dist/api/db/sql-keywords.js +87 -87
  11. package/dist/api/deploy/api.js +60 -0
  12. package/dist/api/deploy/index.js +16 -0
  13. package/dist/api/deploy/schemas.js +105 -0
  14. package/dist/api/deploy/types.js +22 -0
  15. package/dist/api/file/api.js +89 -87
  16. package/dist/api/file/client.js +62 -22
  17. package/dist/api/file/detect.js +3 -3
  18. package/dist/api/file/index.js +2 -1
  19. package/dist/api/file/parsers.js +18 -7
  20. package/dist/api/index.js +7 -1
  21. package/dist/api/observability/api.js +52 -0
  22. package/dist/api/observability/index.js +16 -0
  23. package/dist/api/observability/schemas.js +60 -0
  24. package/dist/api/observability/types.js +27 -0
  25. package/dist/api/plugin/api.js +31 -31
  26. package/dist/cli/commands/app/index.js +62 -0
  27. package/dist/cli/commands/db/index.js +600 -59
  28. package/dist/cli/commands/deploy/index.js +155 -0
  29. package/dist/cli/commands/file/index.js +91 -63
  30. package/dist/cli/commands/index.js +10 -6
  31. package/dist/cli/commands/observability/index.js +240 -0
  32. package/dist/cli/commands/plugin/index.js +27 -27
  33. package/dist/cli/commands/shared.js +86 -10
  34. package/dist/cli/handlers/app/get.js +47 -0
  35. package/dist/cli/handlers/app/index.js +7 -0
  36. package/dist/cli/handlers/app/update.js +59 -0
  37. package/dist/cli/handlers/db/_operator.js +35 -0
  38. package/dist/cli/handlers/db/audit.js +383 -0
  39. package/dist/cli/handlers/db/changelog.js +160 -0
  40. package/dist/cli/handlers/db/data.js +34 -34
  41. package/dist/cli/handlers/db/index.js +17 -1
  42. package/dist/cli/handlers/db/migration.js +245 -0
  43. package/dist/cli/handlers/db/quota.js +68 -0
  44. package/dist/cli/handlers/db/recovery.js +387 -0
  45. package/dist/cli/handlers/db/schema.js +35 -36
  46. package/dist/cli/handlers/db/sql.js +70 -71
  47. package/dist/cli/handlers/deploy/deploy.js +84 -0
  48. package/dist/cli/handlers/deploy/error-log.js +60 -0
  49. package/dist/cli/handlers/deploy/format.js +39 -0
  50. package/dist/cli/handlers/deploy/get.js +71 -0
  51. package/dist/cli/handlers/deploy/helpers.js +41 -0
  52. package/dist/cli/handlers/deploy/history.js +70 -0
  53. package/dist/cli/handlers/deploy/index.js +14 -0
  54. package/dist/cli/handlers/deploy/polling.js +162 -0
  55. package/dist/cli/handlers/file/cp.js +31 -32
  56. package/dist/cli/handlers/file/index.js +3 -1
  57. package/dist/cli/handlers/file/ls.js +6 -7
  58. package/dist/cli/handlers/file/quota.js +66 -0
  59. package/dist/cli/handlers/file/rm.js +33 -32
  60. package/dist/cli/handlers/file/sign.js +4 -5
  61. package/dist/cli/handlers/file/stat.js +11 -11
  62. package/dist/cli/handlers/observability/analytics.js +212 -0
  63. package/dist/cli/handlers/observability/helpers.js +66 -0
  64. package/dist/cli/handlers/observability/index.js +12 -0
  65. package/dist/cli/handlers/observability/log.js +94 -0
  66. package/dist/cli/handlers/observability/metric.js +208 -0
  67. package/dist/cli/handlers/observability/trace.js +102 -0
  68. package/dist/cli/handlers/plugin/plugin-local.js +53 -53
  69. package/dist/cli/handlers/plugin/plugin.js +15 -15
  70. package/dist/cli/help.js +16 -16
  71. package/dist/main.js +13 -9
  72. package/dist/utils/args.js +8 -0
  73. package/dist/utils/colors.js +2 -2
  74. package/dist/utils/config.js +2 -2
  75. package/dist/utils/devops-error.js +28 -0
  76. package/dist/utils/error.js +2 -2
  77. package/dist/utils/git.js +29 -0
  78. package/dist/utils/http.js +119 -1
  79. package/dist/utils/index.js +15 -1
  80. package/dist/utils/output.js +373 -20
  81. package/dist/utils/poll.js +35 -0
  82. package/dist/utils/render.js +27 -27
  83. package/dist/utils/spinner.js +46 -0
  84. package/dist/utils/time.js +208 -0
  85. package/package.json +7 -5
@@ -0,0 +1,387 @@
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。
65
+ // tables_affected 从 preview 拿;elapsed 用 CLI 端墙钟(dataloom apply 是异步
66
+ // 触发,server-side elapsed=0;下面 poll 真终态后用本地计时器算 elapsed)。
67
+ const preview = await runRecoveryPreview(appId, ts);
68
+ const tablesAffected = preview.tablesAffected ?? preview.changes?.length ?? 0;
69
+ // 0 changes 短路:目标时间点与当前一致,apply 没意义。pretty 模式渲染 preview
70
+ // 的 "No changes — database is already at this state." 后直接退;--json 模式
71
+ // 返 status="no_changes" envelope 让下游识别。不进 confirm 也不下发 apply。
72
+ if ((preview.changes?.length ?? 0) === 0) {
73
+ if ((0, output_1.isJsonMode)()) {
74
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
75
+ status: 'no_changes',
76
+ target: ts,
77
+ tablesAffected: 0,
78
+ elapsedSeconds: 0,
79
+ }));
80
+ }
81
+ else {
82
+ renderDiff(ts, preview);
83
+ }
84
+ return;
85
+ }
86
+ if (!opts.yes && !(0, output_1.isJsonMode)()) {
87
+ renderDiff(ts, preview);
88
+ const ok = await confirm(`? Restore database to ${ts}? This will overwrite current data. (y/N) `);
89
+ if (!ok) {
90
+ (0, output_1.emit)('Aborted.');
91
+ return;
92
+ }
93
+ }
94
+ let result;
95
+ try {
96
+ result = await api.db.recover({ appId, target: ts, dryRun: false });
97
+ }
98
+ catch (err) {
99
+ throw decorateRecoveryError(err);
100
+ }
101
+ // 关键:墙钟从 apply 触发瞬间开始,poll 真完成时停。dataloom restore 是异步触发,
102
+ // 立即返 status="restore_triggered",CLI 必须 poll GET /db/recovery/status 拿
103
+ // Redis 缓存的 Resetting/Ready/Failed,否则用户会被误导以为已恢复完。
104
+ const startedAt = Date.now();
105
+ await waitRecoveryDone(appId, ts);
106
+ const elapsed = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
107
+ if ((0, output_1.isJsonMode)()) {
108
+ // PRD:{"status": "restored", "target": "...", "tables_affected": N, "elapsed_seconds": M}
109
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
110
+ status: 'restored',
111
+ target: result.target,
112
+ tablesAffected,
113
+ elapsedSeconds: elapsed,
114
+ }));
115
+ return;
116
+ }
117
+ const tty = (0, render_1.isStdoutTty)();
118
+ const body = `Database restored to ${result.target} ` +
119
+ `(${String(tablesAffected)} tables affected, ${String(elapsed)}s elapsed)`;
120
+ (0, output_1.emit)(tty ? colors_1.c.success(`✓ ${body}`) : `OK ${body}`);
121
+ }
122
+ /**
123
+ * 触发 BranchRestore 后 poll 直到任务终态。dataloom 端 Redis 缓存 workspace 级
124
+ * status,归一成 running / success / failed:
125
+ * - running → 继续 poll
126
+ * - success → 退出,CLI 渲染 ✓
127
+ * - failed → 抛 DB_API_k_dl_1300036 + errorMessage
128
+ *
129
+ * 不设固定超时:BranchRestore 实际时长依库大小变化(前端文案示例 1 分钟,大库可能
130
+ * 几分钟),由 pollUntilDone 内置 60 分钟上限兜底,远超正常场景。
131
+ */
132
+ async function waitRecoveryDone(appId, target) {
133
+ try {
134
+ return await (0, poll_1.pollUntilDone)({
135
+ label: 'recovery apply',
136
+ spinnerLabel: 'Restoring database to target time',
137
+ intervalMs: 2000,
138
+ fetch: () => api.db.getRecoveryStatus({ appId }),
139
+ isDone: (cur) => {
140
+ const status = (cur.status || '').toLowerCase();
141
+ if (status === 'success')
142
+ return { done: true, value: cur };
143
+ if (status === 'failed') {
144
+ throw new error_1.AppError('DB_API_k_dl_1300036', cur.errorMessage ?? `recovery to ${target} failed`);
145
+ }
146
+ return { done: false };
147
+ },
148
+ });
149
+ }
150
+ catch (err) {
151
+ throw decorateRecoveryError(err);
152
+ }
153
+ }
154
+ /**
155
+ * 触发 PITR 预览任务并轮询到终态:
156
+ * 1. POST /db/recovery dryRun=true → previewRequestId
157
+ * 2. GET /db/recovery/preview?previewRequestId 直到 previewStatus=success/failed
158
+ *
159
+ * 错误透传:业务错误(窗口超限 / 格式错)由 dataloom 直接 throw,CLI 在 catch 里
160
+ * 补 hint。failed 状态下 dataloom 已把 errorMessage 翻好,CLI 包成 k_dl_1300036
161
+ * 让 decorateRecoveryError 命中窗口超限 hint。
162
+ */
163
+ async function runRecoveryPreview(appId, ts) {
164
+ let triggered;
165
+ try {
166
+ triggered = await api.db.recover({ appId, target: ts, dryRun: true });
167
+ }
168
+ catch (err) {
169
+ throw decorateRecoveryError(err);
170
+ }
171
+ if (triggered.previewRequestId === undefined || triggered.previewRequestId === '') {
172
+ throw new error_1.AppError('INTERNAL_DB_ERROR', 'recovery diff did not return previewRequestId');
173
+ }
174
+ const previewRequestId = triggered.previewRequestId;
175
+ try {
176
+ return await (0, poll_1.pollUntilDone)({
177
+ label: 'recovery preview',
178
+ spinnerLabel: 'Computing recovery preview',
179
+ intervalMs: 1000,
180
+ fetch: () => api.db.getRecoveryPreview({ appId, previewRequestId }),
181
+ isDone: (cur) => {
182
+ // dataloom 内部 pgsvc 返回首字母大写枚举(Pending/Running/Success/Failed),
183
+ // 但 admin-inner thrift 契约里写的是小写。客户端做大小写归一兜底,避免依赖
184
+ // 上游 case 一致性导致死循环。
185
+ const status = cur.previewStatus.toLowerCase();
186
+ if (status === 'success')
187
+ return { done: true, value: cur };
188
+ if (status === 'failed') {
189
+ // 复用 k_dl_1300036(窗口超限 / 预览失败的统一对外码),让 decorateRecoveryError
190
+ // 命中后给 PITR 7 天窗口的 hint。errorMessage 优先用 dataloom 翻好的 PRD 文案。
191
+ throw new error_1.AppError('DB_API_k_dl_1300036', cur.errorMessage ?? 'recovery preview failed');
192
+ }
193
+ return { done: false };
194
+ },
195
+ });
196
+ }
197
+ catch (err) {
198
+ throw decorateRecoveryError(err);
199
+ }
200
+ }
201
+ // ── helpers ──
202
+ /**
203
+ * 把用户传入的时间统一成 ISO 8601 UTC 字符串发给 dataloom。
204
+ *
205
+ * 复用 utils/time.ts::parseTimeToMs(跟 file ls / db changelog / db audit list 同一套
206
+ * 解析器),支持四种形态:
207
+ * - 相对时间:30s / 5m / 2h / 3d / 1w(从 now 往前推)
208
+ * - 仅日期: 2026-04-15 → 本地时区当日 00:00:00
209
+ * - 本地 datetime: 2026-04-15T10:00:00 → 本地时区
210
+ * - ISO 8601 with TZ: 2026-04-15T10:00:00Z 或 2026-04-15T10:00:00+08:00
211
+ *
212
+ * 不接受松散 / 区域性格式(YYYY/MM/DD、'April 1 2026' 等)——parseTimeToMs 内部
213
+ * 用严格 regex 把这些拒掉,避免之前用 new Date(input) 兜底把脏值送到后端的坑。
214
+ */
215
+ function normalizeTimestamp(input) {
216
+ if (input === '') {
217
+ throw new error_1.AppError('ARGS_INVALID', 'target timestamp is required', {
218
+ next_actions: ['Usage: miaoda db recovery diff|apply <timestamp>'],
219
+ });
220
+ }
221
+ return new Date((0, time_1.parseTimeToMs)(input)).toISOString();
222
+ }
223
+ // PRD diff 输出,三套:
224
+ //
225
+ // TTY pretty(缩进 prose、带 Unicode 箭头、表名按列对齐):
226
+ // Recovery preview (→ 2026-04-15T10:00:00Z):
227
+ //
228
+ // tables affected: 2
229
+ // users: +3 rows, -1 row
230
+ // orders: table will be restored
231
+ //
232
+ // estimated time: ~30s
233
+ //
234
+ // non-TTY pretty(管道友好,扁平 TSV、ASCII 箭头、snake_case key、estimated_time 不带 ~/s):
235
+ // Recovery preview (-> 2026-04-15T10:00:00Z):
236
+ // tables_affected\t2
237
+ // users\t+3 rows, -1 row
238
+ // orders\ttable will be restored
239
+ // estimated_time\t30
240
+ //
241
+ // --json:标准 envelope,字段名固定 snake_case。
242
+ function renderDiff(target, preview) {
243
+ const changes = preview.changes ?? [];
244
+ const tablesAffected = preview.tablesAffected ?? changes.length;
245
+ const estimated = preview.estimatedSeconds;
246
+ if ((0, output_1.isJsonMode)()) {
247
+ (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
248
+ target,
249
+ tablesAffected,
250
+ changes: changes.map((c) => ({
251
+ table: c.table,
252
+ inserted: c.inserted,
253
+ deleted: c.deleted,
254
+ action: c.action,
255
+ droppedAt: c.droppedAt,
256
+ })),
257
+ estimatedSeconds: estimated ?? 30,
258
+ }));
259
+ return;
260
+ }
261
+ const tty = (0, render_1.isStdoutTty)();
262
+ if (tty) {
263
+ (0, output_1.emit)(renderDiffTty(target, changes, tablesAffected, estimated));
264
+ return;
265
+ }
266
+ (0, output_1.emit)(renderDiffPipe(target, changes, tablesAffected, estimated));
267
+ }
268
+ function renderDiffTty(target, changes, tablesAffected, estimated) {
269
+ const header = `Recovery preview (→ ${target}):`;
270
+ if (changes.length === 0) {
271
+ return `${header}\n\n No changes — database is already at this state.`;
272
+ }
273
+ // 对齐:所有表名右侧补到 max(len) + 1 个 ":" + 1 个空格再起描述。PRD 用例里
274
+ // "users: " / "orders: " 三/二空格其实就是按 6 个 char 列宽对齐出来的。
275
+ const maxName = Math.max(...changes.map((c) => c.table.length));
276
+ const lines = [header, '', ` tables affected: ${String(tablesAffected)}`];
277
+ for (const c of changes) {
278
+ const pad = ' '.repeat(Math.max(1, maxName - c.table.length + 2));
279
+ lines.push(` ${c.table}:${pad}${describeChange(c)}`);
280
+ }
281
+ // PRD 要求 estimated time 块固定出现(即便 dataloom 当前没填 EstimatedSeconds,
282
+ // CLI 也用 30s 兜底——PRD 示例展示的就是 30s,统一一个不至于误导的常量)。
283
+ lines.push('');
284
+ lines.push(` estimated time: ~${String(estimated ?? 30)}s`);
285
+ return lines.join('\n');
286
+ }
287
+ function renderDiffPipe(target, changes, tablesAffected, estimated) {
288
+ // 管道场景:去掉缩进,TAB 分列,箭头降级为 ->,key 用 snake_case,
289
+ // estimated_time 只输出秒数(不带 ~/s),方便 awk/grep。
290
+ const header = `Recovery preview (-> ${target}):`;
291
+ if (changes.length === 0) {
292
+ return `${header}\nNo changes — database is already at this state.`;
293
+ }
294
+ const lines = [header, `tables_affected\t${String(tablesAffected)}`];
295
+ for (const c of changes) {
296
+ lines.push(`${c.table}\t${describeChange(c)}`);
297
+ }
298
+ // 与 TTY 一致,始终输出 estimated_time(默认 30),让 awk 解析时不用兜底。
299
+ lines.push(`estimated_time\t${String(estimated ?? 30)}`);
300
+ return lines.join('\n');
301
+ }
302
+ function describeChange(c) {
303
+ // dataloom 端 action 是 PRD 三态 + unavailable 边界:
304
+ // restore_table — schema diff 显示该表在目标时间点存在但当前没有
305
+ // drop_table — 该表当前有但目标时间点没有
306
+ // alter_table — 两侧都在但结构有差异(列 / 索引 / 关系等)
307
+ // unavailable — PITR diff 算不出来,droppedAt 字段复用透传 message
308
+ // 没 action 时是数据行数变化,走下面的 +N / -N 渲染。
309
+ if (c.action === 'restore_table') {
310
+ return 'table will be restored';
311
+ }
312
+ if (c.action === 'drop_table') {
313
+ return 'table will be dropped';
314
+ }
315
+ if (c.action === 'alter_table') {
316
+ return 'table will be altered';
317
+ }
318
+ if (c.action === 'unavailable') {
319
+ return `diff unavailable${c.droppedAt !== undefined && c.droppedAt !== '' ? `: ${c.droppedAt}` : ''}`;
320
+ }
321
+ // 数据变更:+N rows / -N rows
322
+ const parts = [];
323
+ if (c.inserted !== undefined && c.inserted !== 0)
324
+ parts.push(`+${String(c.inserted)} rows`);
325
+ if (c.deleted !== undefined && c.deleted !== 0) {
326
+ parts.push(`-${String(c.deleted)} ${c.deleted === 1 ? 'row' : 'rows'}`);
327
+ }
328
+ return parts.length === 0 ? 'no changes' : parts.join(', ');
329
+ }
330
+ /**
331
+ * 解析 dataloom 嵌在 message 末尾的窗口边界,形如 `[2026-04-09T12:00:00Z ~ 2026-04-16T12:00:00Z]`。
332
+ * 命中返 `{start, end}`,否则 null。两侧时间字符串原样透传给 hint,不做归一化。
333
+ */
334
+ function parseWindowBounds(message) {
335
+ const m = /\[([^[\]]+?)\s*~\s*([^[\]]+?)\]\s*$/.exec(message);
336
+ if (!m)
337
+ return null;
338
+ const start = m[1].trim();
339
+ const end = m[2].trim();
340
+ if (start === '' || end === '')
341
+ return null;
342
+ return { start, end };
343
+ }
344
+ // decorateRecoveryError 给 recovery 路径上的几个错误码补 PRD 规定的 hint。
345
+ // dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
346
+ function decorateRecoveryError(err) {
347
+ if (!(err instanceof error_1.AppError))
348
+ return err;
349
+ switch (err.code) {
350
+ case 'DB_API_k_dl_1300036': {
351
+ // 窗口超限:dataloom 端会把当前窗口边界拼成 `[<startISO> ~ <endISO>]` 嵌到 message
352
+ // 末尾,CLI 在这里提取出来按 PRD 格式 hint。提取失败时退回通用提示。
353
+ const bounds = parseWindowBounds(err.message);
354
+ if (bounds === null) {
355
+ return new error_1.AppError(err.code, err.message, {
356
+ next_actions: [
357
+ 'PITR window is up to 7 days back, limited by your last `db migration apply` time.',
358
+ ],
359
+ });
360
+ }
361
+ // message 里把 [start ~ end] 段移除掉,避免重复
362
+ const cleanMsg = err.message.replace(/\s*\[[^\]]*\]\s*$/, '');
363
+ return new error_1.AppError(err.code, cleanMsg, {
364
+ next_actions: [
365
+ `Current recoverable window: ${bounds.start} ~ ${bounds.end}`,
366
+ `(limited by last migration apply at ${bounds.start})`,
367
+ ],
368
+ });
369
+ }
370
+ case 'DB_API_k_dl_1300038':
371
+ // 时间格式错误:引导 ISO 8601
372
+ return new error_1.AppError(err.code, err.message, {
373
+ next_actions: ['Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z'],
374
+ });
375
+ default:
376
+ return err;
377
+ }
378
+ }
379
+ async function confirm(prompt) {
380
+ const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
381
+ return new Promise((resolve) => {
382
+ rl.question(prompt, (answer) => {
383
+ rl.close();
384
+ resolve(answer.trim().toLowerCase() === 'y');
385
+ });
386
+ });
387
+ }
@@ -39,15 +39,14 @@ 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
- format: "schema",
49
+ format: 'schema',
51
50
  includeStats: true,
52
51
  dbBranch: opts.env,
53
52
  });
@@ -57,30 +56,30 @@ async function handleDbSchemaList(opts) {
57
56
  return;
58
57
  }
59
58
  if (tables.length === 0) {
60
- (0, output_1.emit)("No tables found.");
59
+ (0, output_1.emit)('No tables found.');
61
60
  return;
62
61
  }
63
62
  const tty = (0, render_1.isStdoutTty)();
64
63
  // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)。
65
64
  // updated_at 暂时不展示——PG pg_catalog 不存真实表时间,详见 renderDetail 注释。
66
65
  const headers = [
67
- "name",
68
- "description",
69
- "estimated_row_count",
70
- tty ? "size" : "size_bytes",
71
- "columns",
66
+ 'name',
67
+ 'description',
68
+ 'estimated_row_count',
69
+ tty ? 'size' : 'size_bytes',
70
+ 'columns',
72
71
  ];
73
72
  const rows = tables.map((t) => [
74
73
  t.name,
75
- t.description ?? "",
76
- t.estimated_row_count === null ? "" : String(t.estimated_row_count),
77
- t.size_bytes === null ? "" : tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes),
74
+ t.description ?? '',
75
+ t.estimated_row_count === null ? '' : String(t.estimated_row_count),
76
+ t.size_bytes === null ? '' : tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes),
78
77
  String(t.columns),
79
78
  ]);
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);
@@ -88,13 +87,13 @@ async function handleDbSchemaGet(table, opts) {
88
87
  // non-TTY 或 --ddl:直接取完整 DDL 输出
89
88
  const ddlResp = await api.db.getSchema({
90
89
  appId,
91
- format: "ddl",
90
+ format: 'ddl',
92
91
  tableNames: table,
93
92
  dbBranch: opts.env,
94
93
  });
95
94
  const sql = ddlResp.ddl?.[table];
96
95
  if (!sql) {
97
- throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
96
+ throw new error_1.AppError('TABLE_NOT_FOUND', `Table '${table}' does not exist`, {
98
97
  next_actions: [
99
98
  `Did you mean another table? Run "miaoda db schema list" to see all tables.`,
100
99
  ],
@@ -106,14 +105,14 @@ async function handleDbSchemaGet(table, opts) {
106
105
  // TTY / JSON:取结构化
107
106
  const resp = await api.db.getSchema({
108
107
  appId,
109
- format: "schema",
108
+ format: 'schema',
110
109
  tableNames: table,
111
110
  includeStats: true,
112
111
  dbBranch: opts.env,
113
112
  });
114
113
  const detail = (0, index_1.pickTableDetail)(resp.schema, table);
115
114
  if (!detail) {
116
- throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
115
+ throw new error_1.AppError('TABLE_NOT_FOUND', `Table '${table}' does not exist`, {
117
116
  next_actions: [`Did you mean another table? Run "miaoda db schema list" to see all tables.`],
118
117
  });
119
118
  }
@@ -124,54 +123,54 @@ async function handleDbSchemaGet(table, opts) {
124
123
  (0, output_1.emit)(renderDetail(detail, tty));
125
124
  }
126
125
  function renderDetail(d, tty) {
127
- const systemFields = d.columns.filter((c) => c.name.startsWith("_"));
128
- const userFields = d.columns.filter((c) => !c.name.startsWith("_"));
126
+ const systemFields = d.columns.filter((c) => c.name.startsWith('_'));
127
+ const userFields = d.columns.filter((c) => !c.name.startsWith('_'));
129
128
  // header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size。
130
129
  // 不展示 Created / Updated:PG pg_catalog 不存表创建时间,dataloom 用 OID
131
130
  // 构造的伪时间戳(baseTime=2020-01-01 + OID 秒偏移),仅保排序意义、绝对值
132
131
  // 误导性强,先去掉。后续如果接 ddl_change_log 取真实时间再加回。
133
132
  const header = [
134
133
  // 表名做 cyan 强调(spec:spec 里的"命令名/表名"类强调值都走 highlight)
135
- ["Name", tty ? colors_1.c.highlight(d.name) : d.name],
136
- ["Description", d.description ?? ""],
134
+ ['Name', tty ? colors_1.c.highlight(d.name) : d.name],
135
+ ['Description', d.description ?? ''],
137
136
  [
138
- "Columns",
137
+ 'Columns',
139
138
  systemFields.length > 0
140
139
  ? `${String(userFields.length)} (+ ${String(systemFields.length)} system)`
141
140
  : String(userFields.length),
142
141
  ],
143
- ["Estimated Rows", d.estimated_row_count === null ? "" : String(d.estimated_row_count)],
144
- ["Size", d.size_bytes === null ? "" : (0, render_1.formatSize)(d.size_bytes)],
142
+ ['Estimated Rows', d.estimated_row_count === null ? '' : String(d.estimated_row_count)],
143
+ ['Size', d.size_bytes === null ? '' : (0, render_1.formatSize)(d.size_bytes)],
145
144
  ];
146
- const colHeaders = ["column", "type", "nullable", "default", "comment"];
145
+ const colHeaders = ['column', 'type', 'nullable', 'default', 'comment'];
147
146
  const colRows = userFields.map((c) => [
148
147
  c.name,
149
148
  c.type,
150
- c.nullable ? "yes" : "no",
151
- c.default ?? "",
152
- c.comment ?? "",
149
+ c.nullable ? 'yes' : 'no',
150
+ c.default ?? '',
151
+ c.comment ?? '',
153
152
  ]);
154
153
  const parts = [];
155
154
  parts.push((0, render_1.renderKeyValue)(header, tty));
156
- parts.push("");
155
+ parts.push('');
157
156
  parts.push(tty ? (0, render_1.renderAlignedTable)(colHeaders, colRows) : (0, render_1.renderTsv)(colHeaders, colRows));
158
157
  // Constraints 段(PRIMARY KEY / UNIQUE):表级约束独立成段,与普通索引分离展示
159
158
  if (d.constraints.length > 0) {
160
- parts.push("");
161
- parts.push(tty ? " Constraints:" : "Constraints:");
159
+ parts.push('');
160
+ parts.push(tty ? ' Constraints:' : 'Constraints:');
162
161
  for (const c of d.constraints) {
163
- const line = `${c.type} (${c.columns.join(", ")})`;
162
+ const line = `${c.type} (${c.columns.join(', ')})`;
164
163
  parts.push(tty ? ` ${line}` : line);
165
164
  }
166
165
  }
167
166
  // Indexes 段:普通索引,格式 "<name> ON <col1, col2> USING <method>"
168
167
  if (d.indexes.length > 0) {
169
- parts.push("");
170
- parts.push(tty ? " Indexes:" : "Indexes:");
168
+ parts.push('');
169
+ parts.push(tty ? ' Indexes:' : 'Indexes:');
171
170
  for (const idx of d.indexes) {
172
- const line = `${idx.name} ON ${idx.columns.join(", ")} USING ${idx.method}`;
171
+ const line = `${idx.name} ON ${idx.columns.join(', ')} USING ${idx.method}`;
173
172
  parts.push(tty ? ` ${line}` : line);
174
173
  }
175
174
  }
176
- return parts.join("\n");
175
+ return parts.join('\n');
177
176
  }