@lark-apaas/miaoda-cli 0.1.3 → 0.1.4

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 (71) hide show
  1. package/dist/api/app/api.js +3 -3
  2. package/dist/api/app/schemas.js +43 -43
  3. package/dist/api/db/api.js +398 -55
  4. package/dist/api/db/client.js +155 -28
  5. package/dist/api/db/index.js +12 -1
  6. package/dist/api/db/parsers.js +20 -20
  7. package/dist/api/db/sql-keywords.js +87 -87
  8. package/dist/api/deploy/api.js +5 -5
  9. package/dist/api/deploy/schemas.js +32 -32
  10. package/dist/api/file/api.js +89 -87
  11. package/dist/api/file/client.js +62 -22
  12. package/dist/api/file/detect.js +3 -3
  13. package/dist/api/file/index.js +2 -1
  14. package/dist/api/file/parsers.js +18 -7
  15. package/dist/api/observability/api.js +6 -6
  16. package/dist/api/observability/schemas.js +14 -14
  17. package/dist/api/plugin/api.js +31 -31
  18. package/dist/cli/commands/app/index.js +12 -12
  19. package/dist/cli/commands/db/index.js +602 -54
  20. package/dist/cli/commands/deploy/index.js +28 -28
  21. package/dist/cli/commands/file/index.js +85 -58
  22. package/dist/cli/commands/observability/index.js +69 -69
  23. package/dist/cli/commands/plugin/index.js +27 -27
  24. package/dist/cli/commands/shared.js +10 -10
  25. package/dist/cli/handlers/app/update.js +2 -2
  26. package/dist/cli/handlers/db/_destructive.js +67 -0
  27. package/dist/cli/handlers/db/_env.js +26 -0
  28. package/dist/cli/handlers/db/_operator.js +35 -0
  29. package/dist/cli/handlers/db/audit.js +383 -0
  30. package/dist/cli/handlers/db/changelog.js +160 -0
  31. package/dist/cli/handlers/db/data.js +32 -31
  32. package/dist/cli/handlers/db/index.js +17 -1
  33. package/dist/cli/handlers/db/migration.js +234 -0
  34. package/dist/cli/handlers/db/quota.js +68 -0
  35. package/dist/cli/handlers/db/recovery.js +413 -0
  36. package/dist/cli/handlers/db/schema.js +33 -33
  37. package/dist/cli/handlers/db/sql.js +69 -69
  38. package/dist/cli/handlers/deploy/deploy.js +4 -4
  39. package/dist/cli/handlers/deploy/error-log.js +1 -1
  40. package/dist/cli/handlers/deploy/get.js +3 -3
  41. package/dist/cli/handlers/deploy/polling.js +11 -11
  42. package/dist/cli/handlers/file/cp.js +30 -30
  43. package/dist/cli/handlers/file/index.js +3 -1
  44. package/dist/cli/handlers/file/ls.js +5 -5
  45. package/dist/cli/handlers/file/quota.js +66 -0
  46. package/dist/cli/handlers/file/rm.js +32 -30
  47. package/dist/cli/handlers/file/sign.js +3 -3
  48. package/dist/cli/handlers/file/stat.js +10 -9
  49. package/dist/cli/handlers/observability/analytics.js +47 -47
  50. package/dist/cli/handlers/observability/helpers.js +2 -2
  51. package/dist/cli/handlers/observability/log.js +9 -9
  52. package/dist/cli/handlers/observability/metric.js +26 -26
  53. package/dist/cli/handlers/observability/trace.js +5 -5
  54. package/dist/cli/handlers/plugin/plugin-local.js +53 -53
  55. package/dist/cli/handlers/plugin/plugin.js +15 -15
  56. package/dist/cli/help.js +16 -16
  57. package/dist/main.js +12 -12
  58. package/dist/utils/args.js +1 -1
  59. package/dist/utils/colors.js +2 -2
  60. package/dist/utils/config.js +2 -2
  61. package/dist/utils/devops-error.js +9 -9
  62. package/dist/utils/error.js +2 -2
  63. package/dist/utils/git.js +4 -4
  64. package/dist/utils/http.js +19 -19
  65. package/dist/utils/index.js +3 -1
  66. package/dist/utils/output.js +67 -45
  67. package/dist/utils/poll.js +35 -0
  68. package/dist/utils/render.js +27 -27
  69. package/dist/utils/spinner.js +46 -0
  70. package/dist/utils/time.js +47 -42
  71. package/package.json +1 -1
@@ -6,6 +6,7 @@ exports.emit = emit;
6
6
  exports.emitError = emitError;
7
7
  exports.emitOk = emitOk;
8
8
  exports.emitPaged = emitPaged;
9
+ exports.snakeCaseKeys = snakeCaseKeys;
9
10
  const config_1 = require("./config");
10
11
  const error_1 = require("./error");
11
12
  const colors_1 = require("./colors");
@@ -25,13 +26,13 @@ const SERVER_ERROR_HINTS = {
25
26
  // key 用 BIZ_ERR_MAP 映射后的语义 code(不是原始服务端 k_dl_xxx),保持
26
27
  // 与 help / 文档里宣传的 RESULT_SET_TOO_LARGE 一致。
27
28
  RESULT_SET_TOO_LARGE: [
28
- "Add `LIMIT <n>` to your SQL to narrow the result.",
29
- "For counts, use `SELECT count(*) FROM ...`; for aggregation, use GROUP BY with filters.",
29
+ 'Add `LIMIT <n>` to your SQL to narrow the result.',
30
+ 'For counts, use `SELECT count(*) FROM ...`; for aggregation, use GROUP BY with filters.',
30
31
  ],
31
32
  };
32
33
  function isJsonMode() {
33
34
  const cfg = (0, config_1.getConfig)();
34
- return cfg.output === "json" || Boolean(cfg.json);
35
+ return cfg.output === 'json' || Boolean(cfg.json);
35
36
  }
36
37
  // ── 默认 cell 格式化 ────────────
37
38
  /**
@@ -44,24 +45,24 @@ function isJsonMode() {
44
45
  */
45
46
  function defaultFormat(v) {
46
47
  if (v === null || v === undefined)
47
- return "";
48
- if (typeof v === "string")
48
+ return '';
49
+ if (typeof v === 'string')
49
50
  return v;
50
- if (typeof v === "boolean" || typeof v === "number" || typeof v === "bigint")
51
+ if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'bigint')
51
52
  return String(v);
52
53
  const s = JSON.stringify(v);
53
54
  if (process.stdout.isTTY && s.length > 60)
54
- return s.slice(0, 57) + "...";
55
+ return s.slice(0, 57) + '...';
55
56
  return s;
56
57
  }
57
58
  // ── 时间戳 / 时长 formatter ────────────
58
59
  function toMs(v, divisor) {
59
60
  let n;
60
- if (typeof v === "number")
61
+ if (typeof v === 'number')
61
62
  n = v;
62
- else if (typeof v === "bigint")
63
+ else if (typeof v === 'bigint')
63
64
  n = Number(v);
64
- else if (typeof v === "string") {
65
+ else if (typeof v === 'string') {
65
66
  n = Number(v);
66
67
  if (!Number.isFinite(n))
67
68
  return null;
@@ -73,7 +74,7 @@ function toMs(v, divisor) {
73
74
  return n / divisor;
74
75
  }
75
76
  /** renderDate 默认模板:本地时区、人读友好。业务层可在 fmt.{ns,us,ms,sec}() 里覆盖。 */
76
- const DEFAULT_DATE_TEMPLATE = "yyyy-MM-dd HH:mm:ss";
77
+ const DEFAULT_DATE_TEMPLATE = 'yyyy-MM-dd HH:mm:ss';
77
78
  /**
78
79
  * 把 Date 渲染成模板字符串;time zone 走运行时 local。
79
80
  * 支持的占位符:yyyy / MM / dd / HH / mm / ss / SSS(毫秒)。
@@ -86,14 +87,14 @@ function renderDate(date, template = DEFAULT_DATE_TEMPLATE) {
86
87
  // if (diffMs < 3_600_000) return `${String(Math.floor(diffMs / 60_000))}m ago`;
87
88
  // if (diffMs < 86_400_000) return `${String(Math.floor(diffMs / 3_600_000))}h ago`;
88
89
  // if (diffMs < 7 * 86_400_000) return `${String(Math.floor(diffMs / 86_400_000))}d ago`;
89
- const pad2 = (n) => String(n).padStart(2, "0");
90
+ const pad2 = (n) => String(n).padStart(2, '0');
90
91
  const Y = String(date.getFullYear());
91
92
  const Mo = pad2(date.getMonth() + 1);
92
93
  const D = pad2(date.getDate());
93
94
  const H = pad2(date.getHours());
94
95
  const Mi = pad2(date.getMinutes());
95
96
  const S = pad2(date.getSeconds());
96
- const Ms = String(date.getMilliseconds()).padStart(3, "0");
97
+ const Ms = String(date.getMilliseconds()).padStart(3, '0');
97
98
  // 顺序:先把 SSS 这种长 token 处理掉,再处理短 token,避免子串误覆盖
98
99
  return template
99
100
  .replace(/yyyy/g, Y)
@@ -138,7 +139,7 @@ function makeTruncateFormatter(n) {
138
139
  // 仅 TTY 截断,非 TTY 保留完整值(pipe 下游可解析)
139
140
  if (!process.stdout.isTTY || s.length <= n)
140
141
  return s;
141
- return s.slice(0, Math.max(0, n - 3)) + "...";
142
+ return s.slice(0, Math.max(0, n - 3)) + '...';
142
143
  };
143
144
  }
144
145
  /**
@@ -175,10 +176,10 @@ exports.fmt = {
175
176
  // ── 字段选择 (--json field1,field2) ────────────
176
177
  function getFieldSelection() {
177
178
  const cfg = (0, config_1.getConfig)();
178
- if (typeof cfg.json !== "string")
179
+ if (typeof cfg.json !== 'string')
179
180
  return null;
180
181
  const fields = cfg.json
181
- .split(",")
182
+ .split(',')
182
183
  .map((s) => s.trim())
183
184
  .filter(Boolean);
184
185
  return fields.length > 0 ? fields : null;
@@ -198,22 +199,22 @@ function pickFields(obj, fields) {
198
199
  }
199
200
  function applyFieldSelection(data, fields) {
200
201
  if (Array.isArray(data)) {
201
- return data.map((item) => typeof item === "object" && item !== null && !Array.isArray(item)
202
+ return data.map((item) => typeof item === 'object' && item !== null && !Array.isArray(item)
202
203
  ? pickFields(item, fields)
203
204
  : item);
204
205
  }
205
- if (typeof data === "object" && data !== null) {
206
+ if (typeof data === 'object' && data !== null) {
206
207
  return pickFields(data, fields);
207
208
  }
208
209
  return data;
209
210
  }
210
211
  // ── 信封识别 ────────────
211
- const ENVELOPE_KEYS = new Set(["data", "next_cursor", "has_more"]);
212
+ const ENVELOPE_KEYS = new Set(['data', 'next_cursor', 'has_more']);
212
213
  function isEnvelope(v) {
213
- if (typeof v !== "object" || v === null)
214
+ if (typeof v !== 'object' || v === null)
214
215
  return false;
215
216
  const keys = Object.keys(v);
216
- if (!keys.includes("data"))
217
+ if (!keys.includes('data'))
217
218
  return false;
218
219
  return keys.every((k) => ENVELOPE_KEYS.has(k));
219
220
  }
@@ -234,38 +235,38 @@ function emit(data, schema) {
234
235
  payload = { ...payload, data: applyFieldSelection(payload.data, fields) };
235
236
  }
236
237
  if (isJsonMode()) {
237
- process.stdout.write(JSON.stringify(payload) + "\n");
238
+ process.stdout.write(JSON.stringify(payload) + '\n');
238
239
  return;
239
240
  }
240
241
  if (isEnvelope(payload)) {
241
242
  emitPrettyEnvelope(payload, schema);
242
243
  return;
243
244
  }
244
- if (typeof payload === "string") {
245
- process.stdout.write(payload + "\n");
245
+ if (typeof payload === 'string') {
246
+ process.stdout.write(payload + '\n');
246
247
  }
247
248
  else {
248
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
249
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
249
250
  }
250
251
  }
251
252
  function emitPrettyEnvelope(env, schema) {
252
253
  const { data, next_cursor, has_more } = env;
253
254
  if (Array.isArray(data)) {
254
255
  if (data.length === 0) {
255
- process.stdout.write("(no results)\n");
256
+ process.stdout.write('(no results)\n');
256
257
  }
257
258
  else {
258
259
  writeTable(data, schema);
259
260
  }
260
261
  }
261
- else if (data !== null && typeof data === "object") {
262
+ else if (data !== null && typeof data === 'object') {
262
263
  writeKeyValue(data, schema);
263
264
  }
264
265
  else if (data === null || data === undefined) {
265
- process.stdout.write("(empty)\n");
266
+ process.stdout.write('(empty)\n');
266
267
  }
267
268
  else {
268
- process.stdout.write(defaultFormat(data) + "\n");
269
+ process.stdout.write(defaultFormat(data) + '\n');
269
270
  }
270
271
  if (has_more && next_cursor) {
271
272
  const count = Array.isArray(data) ? data.length : 1;
@@ -318,10 +319,10 @@ function resolveColumnsForRows(items, schema) {
318
319
  return cols;
319
320
  }
320
321
  function writeTable(rows, schema) {
321
- const items = rows.filter((r) => typeof r === "object" && r !== null && !Array.isArray(r));
322
+ const items = rows.filter((r) => typeof r === 'object' && r !== null && !Array.isArray(r));
322
323
  if (items.length === 0) {
323
324
  for (const row of rows)
324
- process.stdout.write(defaultFormat(row) + "\n");
325
+ process.stdout.write(defaultFormat(row) + '\n');
325
326
  return;
326
327
  }
327
328
  const cols = resolveColumnsForRows(items, schema);
@@ -334,14 +335,14 @@ function writeTable(rows, schema) {
334
335
  for (const row of matrix) {
335
336
  const line = row
336
337
  .map((cell, i) => cell.padEnd(widths[i] ?? 0))
337
- .join(" ")
338
+ .join(' ')
338
339
  .trimEnd();
339
- process.stdout.write(line + "\n");
340
+ process.stdout.write(line + '\n');
340
341
  }
341
342
  }
342
343
  else {
343
344
  for (const row of matrix)
344
- process.stdout.write(row.join("\t") + "\n");
345
+ process.stdout.write(row.join('\t') + '\n');
345
346
  }
346
347
  }
347
348
  function resolveColumnsForObject(obj, schema) {
@@ -365,7 +366,7 @@ function resolveColumnsForObject(obj, schema) {
365
366
  function writeKeyValue(obj, schema) {
366
367
  const cols = resolveColumnsForObject(obj, schema);
367
368
  if (cols.length === 0) {
368
- process.stdout.write("(empty)\n");
369
+ process.stdout.write('(empty)\n');
369
370
  return;
370
371
  }
371
372
  if (process.stdout.isTTY) {
@@ -399,34 +400,34 @@ function emitError(err) {
399
400
  };
400
401
  if (hints.length > 0) {
401
402
  // JSON 输出压平成单行,更便于机器消费(脚本 / agent 拼字符串)
402
- errObj.hint = hints.join(" ");
403
+ errObj.hint = hints.join(' ');
403
404
  }
404
- if (typeof info.statement_index === "number") {
405
+ if (typeof info.statement_index === 'number') {
405
406
  errObj.statement_index = info.statement_index;
406
407
  }
407
408
  // PRD 多语句失败 envelope 额外字段:completed / rolled_back
408
409
  if (Array.isArray(info.completed)) {
409
410
  errObj.completed = info.completed;
410
411
  }
411
- if (typeof info.rolled_back === "boolean") {
412
+ if (typeof info.rolled_back === 'boolean') {
412
413
  errObj.rolled_back = info.rolled_back;
413
414
  }
414
- process.stderr.write(JSON.stringify({ error: errObj }) + "\n");
415
+ process.stderr.write(JSON.stringify({ error: errObj }) + '\n');
415
416
  }
416
417
  else {
417
418
  // stderr 染色:picocolors 默认按 stdout.isTTY 判断;stderr 通常也是 tty,
418
419
  // 这里复用 stdout 探测保持简单(stderr only-pipe 的极端场景留作后续优化)
419
420
  // 多语句失败时 Error 行末尾追加 "(at statement K of N)",与 PRD spec 对齐
420
- let errorLine = `${colors_1.c.fail("Error:")} ${info.message}`;
421
- if (typeof info.statement_index === "number") {
421
+ let errorLine = `${colors_1.c.fail('Error:')} ${info.message}`;
422
+ if (typeof info.statement_index === 'number') {
422
423
  const k = info.statement_index + 1;
423
424
  const n = info.total_statements;
424
425
  errorLine +=
425
- typeof n === "number" && n > 0
426
+ typeof n === 'number' && n > 0
426
427
  ? ` (at statement ${String(k)} of ${String(n)})`
427
428
  : ` (at statement ${String(k)})`;
428
429
  }
429
- process.stderr.write(errorLine + "\n");
430
+ process.stderr.write(errorLine + '\n');
430
431
  // 多行 hint:第一行带 "hint:" 标签,后续行用 8 空格缩进对齐 " hint: " 之后的列。
431
432
  // 对应 spec 期望的格式:
432
433
  // Error: ...
@@ -434,7 +435,7 @@ function emitError(err) {
434
435
  // 第二条建议
435
436
  if (hints.length > 0) {
436
437
  const [first, ...rest] = hints;
437
- process.stderr.write(` ${colors_1.c.muted("hint:")} ${first}\n`);
438
+ process.stderr.write(` ${colors_1.c.muted('hint:')} ${first}\n`);
438
439
  for (const line of rest) {
439
440
  process.stderr.write(` ${line}\n`);
440
441
  }
@@ -449,9 +450,30 @@ function emitOk(data) {
449
450
  function emitPaged(items, nextCursor, hasMore) {
450
451
  emit({ data: items, next_cursor: nextCursor, has_more: hasMore });
451
452
  }
453
+ /**
454
+ * 把对象 / 数组里所有字符串 key 从 camelCase 转成 snake_case,递归处理嵌套对象。
455
+ * 后端 IDL 返回的 JSON 字段是 camelCase(`storageUsedBytes`),CLI 对外(PRD)
456
+ * 统一 snake_case(`storage_used_bytes`);emit JSON 前过一遍这个函数。
457
+ */
458
+ function snakeCaseKeys(input) {
459
+ if (Array.isArray(input)) {
460
+ return input.map((item) => snakeCaseKeys(item));
461
+ }
462
+ if (input !== null && typeof input === 'object') {
463
+ const out = {};
464
+ for (const [k, v] of Object.entries(input)) {
465
+ out[camelToSnake(k)] = snakeCaseKeys(v);
466
+ }
467
+ return out;
468
+ }
469
+ return input;
470
+ }
471
+ function camelToSnake(s) {
472
+ return s.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase());
473
+ }
452
474
  function toErrorInfo(err) {
453
475
  if (err instanceof error_1.AppError)
454
476
  return err.toJSON();
455
477
  const message = err instanceof Error ? err.message : String(err);
456
- return { code: "UNKNOWN", message, retryable: false, next_actions: [] };
478
+ return { code: 'UNKNOWN', message, retryable: false, next_actions: [] };
457
479
  }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pollUntilDone = pollUntilDone;
4
+ const error_1 = require("../utils/error");
5
+ const spinner_1 = require("../utils/spinner");
6
+ async function pollUntilDone(opts) {
7
+ const interval = opts.intervalMs ?? 1000;
8
+ const timeout = opts.timeoutMs ?? 300_000;
9
+ const deadline = Date.now() + timeout;
10
+ // 仅当传 spinnerLabel 时启动 spinner;非 TTY / JSON 模式内部自动 noop
11
+ const stopSpinner = opts.spinnerLabel ? (0, spinner_1.startSpinner)(opts.spinnerLabel) : () => undefined;
12
+ try {
13
+ // 立即拉一次(绝大多数轻量任务在 dataloom 端已是同步语义,第一次 fetch 就能拿到 success)
14
+ for (;;) {
15
+ const cur = await opts.fetch();
16
+ const verdict = opts.isDone(cur);
17
+ if (verdict.done)
18
+ return verdict.value;
19
+ if (Date.now() + interval > deadline) {
20
+ throw new error_1.AppError('TASK_TIMEOUT', `${opts.label} did not complete within ${String(Math.round(timeout / 1000))}s`, {
21
+ next_actions: [
22
+ 'The task may still be running server-side. Retry the command, or check `miaoda db migration diff` to verify final state.',
23
+ ],
24
+ });
25
+ }
26
+ await sleep(interval);
27
+ }
28
+ }
29
+ finally {
30
+ stopSpinner();
31
+ }
32
+ }
33
+ function sleep(ms) {
34
+ return new Promise((resolve) => setTimeout(resolve, ms));
35
+ }
@@ -28,7 +28,7 @@ const colors_1 = require("./colors");
28
28
  /** 将字节数格式化为人类可读(`24 KB` / `2.1 MB` / `1.5 GB`)。 */
29
29
  function formatSize(bytes) {
30
30
  if (!Number.isFinite(bytes) || bytes < 0)
31
- return "";
31
+ return '';
32
32
  if (bytes >= 1 << 30)
33
33
  return `${(bytes / (1 << 30)).toFixed(1)} GB`;
34
34
  if (bytes >= 1 << 20)
@@ -40,7 +40,7 @@ function formatSize(bytes) {
40
40
  /** 将 ISO 时间转为 TTY 下更友好的相对时间;non-TTY 保持 ISO 原值。 */
41
41
  function formatTime(iso, isTty) {
42
42
  if (!iso)
43
- return "";
43
+ return '';
44
44
  if (!isTty)
45
45
  return iso;
46
46
  const ts = Date.parse(iso);
@@ -65,8 +65,8 @@ function formatTime(iso, isTty) {
65
65
  // 超过 7 天,用 YYYY-MM-DD
66
66
  const date = new Date(ts);
67
67
  const y = String(date.getFullYear());
68
- const mo = String(date.getMonth() + 1).padStart(2, "0");
69
- const da = String(date.getDate()).padStart(2, "0");
68
+ const mo = String(date.getMonth() + 1).padStart(2, '0');
69
+ const da = String(date.getDate()).padStart(2, '0');
70
70
  return `${y}-${mo}-${da}`;
71
71
  }
72
72
  /**
@@ -103,7 +103,7 @@ function charWidth(cp) {
103
103
  }
104
104
  /** cell 可见字符宽度:剥离 ANSI 转义后按终端列宽逐字符累加(CJK 算 2 列)。 */
105
105
  function visibleWidth(s) {
106
- const stripped = s.replace(ANSI_SGR_RE, "");
106
+ const stripped = s.replace(ANSI_SGR_RE, '');
107
107
  let w = 0;
108
108
  for (const c of stripped) {
109
109
  w += charWidth(c.codePointAt(0) ?? 0);
@@ -112,7 +112,7 @@ function visibleWidth(s) {
112
112
  }
113
113
  function padVisibleEnd(s, targetWidth) {
114
114
  const w = visibleWidth(s);
115
- return w >= targetWidth ? s : s + " ".repeat(targetWidth - w);
115
+ return w >= targetWidth ? s : s + ' '.repeat(targetWidth - w);
116
116
  }
117
117
  /** 渲染 TTY 对齐表格(多行,列宽按最长内容;ANSI 转义不计入列宽)。
118
118
  * 表头按 spec 用 bold + cyan 染色;ANSI 序列由 visibleWidth 剥离不影响列宽。 */
@@ -120,7 +120,7 @@ function renderAlignedTable(headers, rows) {
120
120
  const colWidths = headers.map((h, i) => {
121
121
  let w = visibleWidth(h);
122
122
  for (const row of rows) {
123
- const cw = visibleWidth(row[i] || "");
123
+ const cw = visibleWidth(row[i] || '');
124
124
  if (cw > w)
125
125
  w = cw;
126
126
  }
@@ -129,34 +129,34 @@ function renderAlignedTable(headers, rows) {
129
129
  const lines = [];
130
130
  lines.push(headers
131
131
  .map((h, i) => colors_1.c.header(padVisibleEnd(h, colWidths[i])))
132
- .join(" ")
132
+ .join(' ')
133
133
  .trimEnd());
134
134
  for (const row of rows) {
135
135
  lines.push(row
136
- .map((cell, i) => padVisibleEnd(cell || "", colWidths[i]))
137
- .join(" ")
136
+ .map((cell, i) => padVisibleEnd(cell || '', colWidths[i]))
137
+ .join(' ')
138
138
  .trimEnd());
139
139
  }
140
- return lines.join("\n");
140
+ return lines.join('\n');
141
141
  }
142
142
  /** 渲染 non-TTY tab 分隔(字段原值,ISO 时间)。 */
143
143
  function renderTsv(headers, rows) {
144
144
  const lines = [];
145
- lines.push(headers.join("\t"));
145
+ lines.push(headers.join('\t'));
146
146
  for (const row of rows) {
147
- lines.push(row.join("\t"));
147
+ lines.push(row.join('\t'));
148
148
  }
149
- return lines.join("\n");
149
+ return lines.join('\n');
150
150
  }
151
151
  /** 渲染 key-value 多行(用于 stat 等单条详情)。key 右对齐 + bold cyan 染色。 */
152
152
  function renderKeyValue(pairs, isTty) {
153
153
  if (pairs.length === 0)
154
- return "";
154
+ return '';
155
155
  if (!isTty) {
156
- return pairs.map(([k, v]) => `${k}\t${v}`).join("\n");
156
+ return pairs.map(([k, v]) => `${k}\t${v}`).join('\n');
157
157
  }
158
158
  const keyWidth = Math.max(...pairs.map(([k]) => k.length));
159
- return pairs.map(([k, v]) => `${colors_1.c.header(k.padStart(keyWidth))}: ${v}`).join("\n");
159
+ return pairs.map(([k, v]) => `${colors_1.c.header(k.padStart(keyWidth))}: ${v}`).join('\n');
160
160
  }
161
161
  /** 通用 isTTY 判定(stdout 是否交互终端)。Node 运行时 isTTY 为 true 或 undefined;TS 类型上 tty.WriteStream 定义为固定 true,绕开做运行时判断。 */
162
162
  function isStdoutTty() {
@@ -170,15 +170,15 @@ function parseDuration(input) {
170
170
  throw new Error(`Invalid duration: ${input}`);
171
171
  }
172
172
  const n = Number(m[1]);
173
- const unit = m[2] || "s";
173
+ const unit = m[2] || 's';
174
174
  switch (unit) {
175
- case "s":
175
+ case 's':
176
176
  return n;
177
- case "m":
177
+ case 'm':
178
178
  return n * 60;
179
- case "h":
179
+ case 'h':
180
180
  return n * 3600;
181
- case "d":
181
+ case 'd':
182
182
  return n * 86400;
183
183
  default:
184
184
  return n;
@@ -191,15 +191,15 @@ function parseSize(input) {
191
191
  throw new Error(`Invalid size: ${input}`);
192
192
  }
193
193
  const n = Number(m[1]);
194
- const unit = (m[2] || "B").toUpperCase();
194
+ const unit = (m[2] || 'B').toUpperCase();
195
195
  switch (unit) {
196
- case "B":
196
+ case 'B':
197
197
  return Math.round(n);
198
- case "KB":
198
+ case 'KB':
199
199
  return Math.round(n * 1024);
200
- case "MB":
200
+ case 'MB':
201
201
  return Math.round(n * 1024 * 1024);
202
- case "GB":
202
+ case 'GB':
203
203
  return Math.round(n * 1024 * 1024 * 1024);
204
204
  default:
205
205
  return Math.round(n);
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startSpinner = startSpinner;
4
+ const config_1 = require("./config");
5
+ /**
6
+ * TTY 友好的 loading 指示器,写到 stderr 上避免污染 stdout(保证 --json 输出干净)。
7
+ *
8
+ * 设计:
9
+ * - 仅 stderr.isTTY 且非 --json 模式启用;其他情况静默(CI / 管道 / JSON 输出场景)
10
+ * - 80ms 一帧 braille 旋转字符 + 累计 elapsed 秒数
11
+ * - 返回 stop() 函数;调用方 finally 里调用,无论成功失败都清掉 spinner 行
12
+ *
13
+ * 用法:
14
+ * const stop = startSpinner('Applying migration to online');
15
+ * try { await someLongTask(); } finally { stop(); }
16
+ */
17
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
18
+ const HIDE_CURSOR = '[?25l';
19
+ const SHOW_CURSOR = '[?25h';
20
+ const CLEAR_LINE = '\r';
21
+ function startSpinner(label) {
22
+ // 非 TTY / JSON 模式:静默 noop,避免污染管道与结构化输出
23
+ if (!process.stderr.isTTY || (0, config_1.getConfig)().json) {
24
+ return () => {
25
+ /* noop */
26
+ };
27
+ }
28
+ let frame = 0;
29
+ const start = Date.now();
30
+ process.stderr.write(HIDE_CURSOR);
31
+ const render = () => {
32
+ const elapsedSec = Math.round((Date.now() - start) / 1000);
33
+ process.stderr.write(`${CLEAR_LINE}${FRAMES[frame]} ${label}... ${String(elapsedSec)}s`);
34
+ frame = (frame + 1) % FRAMES.length;
35
+ };
36
+ render();
37
+ const timer = setInterval(render, 80);
38
+ let stopped = false;
39
+ return () => {
40
+ if (stopped)
41
+ return;
42
+ stopped = true;
43
+ clearInterval(timer);
44
+ process.stderr.write(`${CLEAR_LINE}${SHOW_CURSOR}`);
45
+ };
46
+ }