@lark-apaas/miaoda-cli 0.1.3 → 0.1.4-alpha.abaa17f

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 (103) 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/index.js +12 -1
  10. package/dist/api/deploy/modern-types.js +23 -0
  11. package/dist/api/deploy/modern.js +70 -0
  12. package/dist/api/deploy/plugin-instances-types.js +6 -0
  13. package/dist/api/deploy/plugin-instances.js +30 -0
  14. package/dist/api/deploy/schemas.js +32 -32
  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/observability/api.js +6 -6
  21. package/dist/api/observability/schemas.js +14 -14
  22. package/dist/api/plugin/api.js +31 -31
  23. package/dist/cli/commands/app/index.js +94 -13
  24. package/dist/cli/commands/db/index.js +602 -54
  25. package/dist/cli/commands/deploy/index.js +28 -28
  26. package/dist/cli/commands/deploy/modern.js +48 -0
  27. package/dist/cli/commands/file/index.js +85 -58
  28. package/dist/cli/commands/index.js +31 -5
  29. package/dist/cli/commands/observability/index.js +69 -69
  30. package/dist/cli/commands/plugin/index.js +27 -27
  31. package/dist/cli/commands/shared.js +10 -10
  32. package/dist/cli/handlers/app/index.js +6 -1
  33. package/dist/cli/handlers/app/init.js +86 -0
  34. package/dist/cli/handlers/app/update.js +2 -2
  35. package/dist/cli/handlers/db/_destructive.js +67 -0
  36. package/dist/cli/handlers/db/_env.js +26 -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 +32 -31
  41. package/dist/cli/handlers/db/index.js +17 -1
  42. package/dist/cli/handlers/db/migration.js +234 -0
  43. package/dist/cli/handlers/db/quota.js +68 -0
  44. package/dist/cli/handlers/db/recovery.js +413 -0
  45. package/dist/cli/handlers/db/schema.js +33 -33
  46. package/dist/cli/handlers/db/sql.js +69 -69
  47. package/dist/cli/handlers/deploy/deploy.js +4 -4
  48. package/dist/cli/handlers/deploy/error-log.js +1 -1
  49. package/dist/cli/handlers/deploy/get.js +3 -3
  50. package/dist/cli/handlers/deploy/modern.js +32 -0
  51. package/dist/cli/handlers/deploy/polling.js +11 -11
  52. package/dist/cli/handlers/file/cp.js +30 -30
  53. package/dist/cli/handlers/file/index.js +3 -1
  54. package/dist/cli/handlers/file/ls.js +5 -5
  55. package/dist/cli/handlers/file/quota.js +66 -0
  56. package/dist/cli/handlers/file/rm.js +32 -30
  57. package/dist/cli/handlers/file/sign.js +3 -3
  58. package/dist/cli/handlers/file/stat.js +10 -9
  59. package/dist/cli/handlers/observability/analytics.js +47 -47
  60. package/dist/cli/handlers/observability/helpers.js +2 -2
  61. package/dist/cli/handlers/observability/log.js +9 -9
  62. package/dist/cli/handlers/observability/metric.js +26 -26
  63. package/dist/cli/handlers/observability/trace.js +5 -5
  64. package/dist/cli/handlers/plugin/plugin-local.js +53 -53
  65. package/dist/cli/handlers/plugin/plugin.js +15 -15
  66. package/dist/cli/help.js +16 -16
  67. package/dist/main.js +12 -12
  68. package/dist/services/app/init/index.js +14 -0
  69. package/dist/services/app/init/install.js +101 -0
  70. package/dist/services/app/init/steering.js +74 -0
  71. package/dist/services/app/init/template.js +85 -0
  72. package/dist/services/deploy/modern/atoms/build.js +54 -0
  73. package/dist/services/deploy/modern/atoms/context.js +30 -0
  74. package/dist/services/deploy/modern/atoms/index.js +17 -0
  75. package/dist/services/deploy/modern/atoms/local-release.js +27 -0
  76. package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
  77. package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
  78. package/dist/services/deploy/modern/atoms/upload.js +192 -0
  79. package/dist/services/deploy/modern/check.js +58 -0
  80. package/dist/services/deploy/modern/constants.js +13 -0
  81. package/dist/services/deploy/modern/index.js +16 -0
  82. package/dist/services/deploy/modern/pipelines/index.js +5 -0
  83. package/dist/services/deploy/modern/pipelines/local.js +75 -0
  84. package/dist/services/deploy/modern/protocol.js +43 -0
  85. package/dist/services/deploy/modern/run-types.js +4 -0
  86. package/dist/services/deploy/modern/run.js +13 -0
  87. package/dist/services/deploy/modern/template-key-map.js +29 -0
  88. package/dist/utils/args.js +1 -1
  89. package/dist/utils/colors.js +2 -2
  90. package/dist/utils/config.js +2 -2
  91. package/dist/utils/devops-error.js +9 -9
  92. package/dist/utils/error.js +2 -2
  93. package/dist/utils/git.js +4 -4
  94. package/dist/utils/http.js +27 -21
  95. package/dist/utils/index.js +3 -1
  96. package/dist/utils/npm-pack.js +55 -0
  97. package/dist/utils/output.js +67 -45
  98. package/dist/utils/poll.js +35 -0
  99. package/dist/utils/render.js +27 -27
  100. package/dist/utils/spark-meta.js +48 -0
  101. package/dist/utils/spinner.js +46 -0
  102. package/dist/utils/time.js +47 -42
  103. package/package.json +1 -1
@@ -60,42 +60,48 @@ function createClient(opts) {
60
60
  instance.interceptors.request.use(applyCanaryHeader);
61
61
  return instance;
62
62
  }
63
- /** 若 MIAODA_CANARY_HEADER 已设置,注入 x-tt-env 小流量头 */
63
+ /**
64
+ * 默认灰度泳道。当前 miaoda CLI 接入的新接口(modern deploy 等)只在该泳道生效,
65
+ * 缺省走这个。MIAODA_CANARY_HEADER 可显式覆盖;设为空字符串则去掉头。
66
+ */
67
+ const DEFAULT_CANARY_HEADER = 'boe_miaoda_doubao';
68
+ /** 注入 x-tt-env 小流量头:env 显式设值 > DEFAULT_CANARY_HEADER */
64
69
  function applyCanaryHeader(reqConfig) {
65
- const canary = process.env.MIAODA_CANARY_HEADER;
70
+ // env 显式设为空字符串视为"去掉灰度头";未设置时用 DEFAULT_CANARY_HEADER
71
+ const canary = process.env.MIAODA_CANARY_HEADER ?? DEFAULT_CANARY_HEADER;
66
72
  if (!canary)
67
73
  return reqConfig;
68
74
  const headers = new Headers(reqConfig.headers);
69
- headers.set("x-tt-env", canary);
75
+ headers.set('x-tt-env', canary);
70
76
  reqConfig.headers = headers;
71
77
  return reqConfig;
72
78
  }
73
79
  /** 走管理端 innerapi 的 POST + 信封解析 */
74
80
  async function postInnerApi(url, body, opts) {
75
- const startMs = logRequestStart("POST", url, body);
81
+ const startMs = logRequestStart('POST', url, body);
76
82
  let response;
77
83
  try {
78
84
  response = await getHttpClient().post(url, body);
79
85
  }
80
86
  catch (err) {
81
- logResponseFailure("POST", url, err, startMs);
87
+ logResponseFailure('POST', url, err, startMs);
82
88
  throw err;
83
89
  }
84
- logResponseEnd("POST", url, response, startMs);
90
+ logResponseEnd('POST', url, response, startMs);
85
91
  return handleInnerEnvelope(response, url, opts);
86
92
  }
87
93
  /** 走管理端 innerapi 的 GET + 信封解析 */
88
94
  async function getInnerApi(url, opts) {
89
- const startMs = logRequestStart("GET", url);
95
+ const startMs = logRequestStart('GET', url);
90
96
  let response;
91
97
  try {
92
98
  response = await getHttpClient().get(url);
93
99
  }
94
100
  catch (err) {
95
- logResponseFailure("GET", url, err, startMs);
101
+ logResponseFailure('GET', url, err, startMs);
96
102
  throw err;
97
103
  }
98
- logResponseEnd("GET", url, response, startMs);
104
+ logResponseEnd('GET', url, response, startMs);
99
105
  return handleInnerEnvelope(response, url, opts);
100
106
  }
101
107
  async function handleInnerEnvelope(response, url, opts) {
@@ -103,10 +109,10 @@ async function handleInnerEnvelope(response, url, opts) {
103
109
  throw new error_1.HttpError(response.status, url, `${opts.errPrefix}: ${String(response.status)} ${response.statusText}`);
104
110
  }
105
111
  const result = await response.json();
106
- if (typeof result === "object" && result !== null && "status_code" in result) {
112
+ if (typeof result === 'object' && result !== null && 'status_code' in result) {
107
113
  const env = result;
108
- if (env.status_code !== undefined && env.status_code !== "0") {
109
- const msg = env.message ?? env.error_msg ?? "unknown error";
114
+ if (env.status_code !== undefined && env.status_code !== '0') {
115
+ const msg = env.message ?? env.error_msg ?? 'unknown error';
110
116
  // verbose: 把完整业务错误信封打到 stderr,便于追溯
111
117
  if ((0, config_1.getConfig)().verbose) {
112
118
  (0, logger_1.debug)(` envelope: ${truncateForLog(safeStringify(env), 1000)}`);
@@ -114,7 +120,7 @@ async function handleInnerEnvelope(response, url, opts) {
114
120
  const mapped = opts.mapErr?.(env.status_code, msg);
115
121
  if (mapped)
116
122
  throw mapped;
117
- throw new error_1.AppError(opts.defaultErrCode ?? "INTERNAL_API_ERROR", `${opts.errPrefix}: ${msg}`);
123
+ throw new error_1.AppError(opts.defaultErrCode ?? 'INTERNAL_API_ERROR', `${opts.errPrefix}: ${msg}`);
118
124
  }
119
125
  // status_code = "0":data 存在则解封;否则 envelope 自身就是业务体
120
126
  if (env.data)
@@ -142,7 +148,7 @@ function logResponseEnd(method, url, response, startMs) {
142
148
  return;
143
149
  const elapsedMs = Date.now() - startMs;
144
150
  const logid = pickLogid(response.headers);
145
- const tail = logid ? ` logid=${logid}` : "";
151
+ const tail = logid ? ` logid=${logid}` : '';
146
152
  (0, logger_1.debug)(`← ${method} ${url} ${String(response.status)} ${String(elapsedMs)}ms${tail}`);
147
153
  }
148
154
  /**
@@ -158,17 +164,17 @@ function logResponseFailure(method, url, err, startMs) {
158
164
  const e = err;
159
165
  const status = e.response?.status;
160
166
  const logid = e.response?.headers ? pickLogid(e.response.headers) : null;
161
- const statusPart = status !== undefined ? String(status) : "ERR";
162
- const logidPart = logid ? ` logid=${logid}` : "";
163
- const msgPart = e.message ? ` (${e.message})` : "";
167
+ const statusPart = status !== undefined ? String(status) : 'ERR';
168
+ const logidPart = logid ? ` logid=${logid}` : '';
169
+ const msgPart = e.message ? ` (${e.message})` : '';
164
170
  (0, logger_1.debug)(`✗ ${method} ${url} ${statusPart} ${String(elapsedMs)}ms${logidPart}${msgPart}`);
165
171
  }
166
172
  /** BAM gateway 在不同 PSM/版本上 logid 落在不同 header;按命中顺序取第一个非空。 */
167
173
  function pickLogid(headers) {
168
- return (headers.get("x-tt-logid") ??
169
- headers.get("x-logid") ??
170
- headers.get("logid") ??
171
- headers.get("x-tt-trace-tag"));
174
+ return (headers.get('x-tt-logid') ??
175
+ headers.get('x-logid') ??
176
+ headers.get('logid') ??
177
+ headers.get('x-tt-trace-tag'));
172
178
  }
173
179
  function safeStringify(v) {
174
180
  try {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.getInnerApi = exports.postInnerApi = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.fmt = exports.isJsonMode = exports.emitPaged = exports.emitOk = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
3
+ exports.pollUntilDone = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.getInnerApi = exports.postInnerApi = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.fmt = exports.isJsonMode = exports.emitPaged = exports.emitOk = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
4
4
  var error_1 = require("./error");
5
5
  Object.defineProperty(exports, "AppError", { enumerable: true, get: function () { return error_1.AppError; } });
6
6
  Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return error_1.HttpError; } });
@@ -37,3 +37,5 @@ Object.defineProperty(exports, "parseToNs", { enumerable: true, get: function ()
37
37
  Object.defineProperty(exports, "parseToSec", { enumerable: true, get: function () { return time_1.parseToSec; } });
38
38
  Object.defineProperty(exports, "msToNs", { enumerable: true, get: function () { return time_1.msToNs; } });
39
39
  Object.defineProperty(exports, "msToSec", { enumerable: true, get: function () { return time_1.msToSec; } });
40
+ var poll_1 = require("./poll");
41
+ Object.defineProperty(exports, "pollUntilDone", { enumerable: true, get: function () { return poll_1.pollUntilDone; } });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fetchNpmPackage = fetchNpmPackage;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const error_1 = require("./error");
12
+ const logger_1 = require("./logger");
13
+ const DEFAULT_REGISTRY = 'https://registry.npmmirror.com/';
14
+ /**
15
+ * `npm pack <pkg>@<ver> --registry <r>` + `tar -xzf`,解到临时目录。
16
+ * extractDir 是包内容根(含 package.json)。使用完必须 cleanup()。
17
+ */
18
+ function fetchNpmPackage(opts) {
19
+ const registry = opts.registry ?? process.env.MIAODA_NPM_REGISTRY ?? DEFAULT_REGISTRY;
20
+ const version = opts.version ?? 'latest';
21
+ const pkgSpec = `${opts.packageName}@${version}`;
22
+ const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-pack-'));
23
+ try {
24
+ (0, logger_1.debug)(`npm pack ${pkgSpec} --registry ${registry} → ${tmpDir}`);
25
+ const stdout = (0, node_child_process_1.execFileSync)('npm', ['pack', pkgSpec, '--pack-destination', tmpDir, '--json', '--registry', registry], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
26
+ const packInfo = JSON.parse(stdout);
27
+ const tgzFilename = packInfo[0]?.filename;
28
+ const pkgVersion = packInfo[0]?.version ?? version;
29
+ if (!tgzFilename) {
30
+ throw new error_1.AppError('NPM_PACK_FAILED', `npm pack 未返回 filename:${pkgSpec}`);
31
+ }
32
+ const tgzPath = node_path_1.default.join(tmpDir, tgzFilename);
33
+ const extractDir = node_path_1.default.join(tmpDir, 'extracted');
34
+ node_fs_1.default.mkdirSync(extractDir, { recursive: true });
35
+ (0, node_child_process_1.execFileSync)('tar', ['-xzf', tgzPath, '-C', extractDir], {
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ });
38
+ return {
39
+ extractDir: node_path_1.default.join(extractDir, 'package'),
40
+ version: pkgVersion,
41
+ cleanup: () => {
42
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
43
+ },
44
+ };
45
+ }
46
+ catch (err) {
47
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
48
+ if (err instanceof error_1.AppError)
49
+ throw err;
50
+ const msg = err instanceof Error ? err.message : String(err);
51
+ throw new error_1.AppError('NPM_PACK_FAILED', `抓取 npm 包失败 (${pkgSpec}): ${msg}`, {
52
+ next_actions: [`确认包已发布到 registry:${registry}`, `检查版本号 / dist-tag 拼写`],
53
+ });
54
+ }
55
+ }
@@ -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
+ }