@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.
- package/dist/api/app/api.js +3 -3
- package/dist/api/app/schemas.js +43 -43
- package/dist/api/db/api.js +398 -55
- package/dist/api/db/client.js +155 -28
- package/dist/api/db/index.js +12 -1
- package/dist/api/db/parsers.js +20 -20
- package/dist/api/db/sql-keywords.js +87 -87
- package/dist/api/deploy/api.js +5 -5
- package/dist/api/deploy/index.js +12 -1
- package/dist/api/deploy/modern-types.js +23 -0
- package/dist/api/deploy/modern.js +70 -0
- package/dist/api/deploy/plugin-instances-types.js +6 -0
- package/dist/api/deploy/plugin-instances.js +30 -0
- package/dist/api/deploy/schemas.js +32 -32
- package/dist/api/file/api.js +89 -87
- package/dist/api/file/client.js +62 -22
- package/dist/api/file/detect.js +3 -3
- package/dist/api/file/index.js +2 -1
- package/dist/api/file/parsers.js +18 -7
- package/dist/api/observability/api.js +6 -6
- package/dist/api/observability/schemas.js +14 -14
- package/dist/api/plugin/api.js +31 -31
- package/dist/cli/commands/app/index.js +94 -13
- package/dist/cli/commands/db/index.js +602 -54
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/deploy/modern.js +48 -0
- package/dist/cli/commands/file/index.js +85 -58
- package/dist/cli/commands/index.js +31 -5
- package/dist/cli/commands/observability/index.js +69 -69
- package/dist/cli/commands/plugin/index.js +27 -27
- package/dist/cli/commands/shared.js +10 -10
- package/dist/cli/handlers/app/index.js +6 -1
- package/dist/cli/handlers/app/init.js +86 -0
- package/dist/cli/handlers/app/update.js +2 -2
- package/dist/cli/handlers/db/_destructive.js +67 -0
- package/dist/cli/handlers/db/_env.js +26 -0
- package/dist/cli/handlers/db/_operator.js +35 -0
- package/dist/cli/handlers/db/audit.js +383 -0
- package/dist/cli/handlers/db/changelog.js +160 -0
- package/dist/cli/handlers/db/data.js +32 -31
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +234 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +413 -0
- package/dist/cli/handlers/db/schema.js +33 -33
- package/dist/cli/handlers/db/sql.js +69 -69
- package/dist/cli/handlers/deploy/deploy.js +4 -4
- package/dist/cli/handlers/deploy/error-log.js +1 -1
- package/dist/cli/handlers/deploy/get.js +3 -3
- package/dist/cli/handlers/deploy/modern.js +32 -0
- package/dist/cli/handlers/deploy/polling.js +11 -11
- package/dist/cli/handlers/file/cp.js +30 -30
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +5 -5
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +32 -30
- package/dist/cli/handlers/file/sign.js +3 -3
- package/dist/cli/handlers/file/stat.js +10 -9
- package/dist/cli/handlers/observability/analytics.js +47 -47
- package/dist/cli/handlers/observability/helpers.js +2 -2
- package/dist/cli/handlers/observability/log.js +9 -9
- package/dist/cli/handlers/observability/metric.js +26 -26
- package/dist/cli/handlers/observability/trace.js +5 -5
- package/dist/cli/handlers/plugin/plugin-local.js +53 -53
- package/dist/cli/handlers/plugin/plugin.js +15 -15
- package/dist/cli/help.js +16 -16
- package/dist/main.js +12 -12
- package/dist/services/app/init/index.js +14 -0
- package/dist/services/app/init/install.js +101 -0
- package/dist/services/app/init/steering.js +74 -0
- package/dist/services/app/init/template.js +85 -0
- package/dist/services/deploy/modern/atoms/build.js +54 -0
- package/dist/services/deploy/modern/atoms/context.js +30 -0
- package/dist/services/deploy/modern/atoms/index.js +17 -0
- package/dist/services/deploy/modern/atoms/local-release.js +27 -0
- package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
- package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
- package/dist/services/deploy/modern/atoms/upload.js +192 -0
- package/dist/services/deploy/modern/check.js +58 -0
- package/dist/services/deploy/modern/constants.js +13 -0
- package/dist/services/deploy/modern/index.js +16 -0
- package/dist/services/deploy/modern/pipelines/index.js +5 -0
- package/dist/services/deploy/modern/pipelines/local.js +75 -0
- package/dist/services/deploy/modern/protocol.js +43 -0
- package/dist/services/deploy/modern/run-types.js +4 -0
- package/dist/services/deploy/modern/run.js +13 -0
- package/dist/services/deploy/modern/template-key-map.js +29 -0
- package/dist/utils/args.js +1 -1
- package/dist/utils/colors.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/devops-error.js +9 -9
- package/dist/utils/error.js +2 -2
- package/dist/utils/git.js +4 -4
- package/dist/utils/http.js +27 -21
- package/dist/utils/index.js +3 -1
- package/dist/utils/npm-pack.js +55 -0
- package/dist/utils/output.js +67 -45
- package/dist/utils/poll.js +35 -0
- package/dist/utils/render.js +27 -27
- package/dist/utils/spark-meta.js +48 -0
- package/dist/utils/spinner.js +46 -0
- package/dist/utils/time.js +47 -42
- package/package.json +1 -1
package/dist/utils/http.js
CHANGED
|
@@ -60,42 +60,48 @@ function createClient(opts) {
|
|
|
60
60
|
instance.interceptors.request.use(applyCanaryHeader);
|
|
61
61
|
return instance;
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
87
|
+
logResponseFailure('POST', url, err, startMs);
|
|
82
88
|
throw err;
|
|
83
89
|
}
|
|
84
|
-
logResponseEnd(
|
|
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(
|
|
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(
|
|
101
|
+
logResponseFailure('GET', url, err, startMs);
|
|
96
102
|
throw err;
|
|
97
103
|
}
|
|
98
|
-
logResponseEnd(
|
|
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 ===
|
|
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 !==
|
|
109
|
-
const msg = env.message ?? env.error_msg ??
|
|
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 ??
|
|
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) :
|
|
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(
|
|
169
|
-
headers.get(
|
|
170
|
-
headers.get(
|
|
171
|
-
headers.get(
|
|
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 {
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/utils/output.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
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 ===
|
|
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 ===
|
|
48
|
+
return '';
|
|
49
|
+
if (typeof v === 'string')
|
|
49
50
|
return v;
|
|
50
|
-
if (typeof v ===
|
|
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 ===
|
|
61
|
+
if (typeof v === 'number')
|
|
61
62
|
n = v;
|
|
62
|
-
else if (typeof v ===
|
|
63
|
+
else if (typeof v === 'bigint')
|
|
63
64
|
n = Number(v);
|
|
64
|
-
else if (typeof v ===
|
|
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 =
|
|
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,
|
|
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,
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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([
|
|
212
|
+
const ENVELOPE_KEYS = new Set(['data', 'next_cursor', 'has_more']);
|
|
212
213
|
function isEnvelope(v) {
|
|
213
|
-
if (typeof v !==
|
|
214
|
+
if (typeof v !== 'object' || v === null)
|
|
214
215
|
return false;
|
|
215
216
|
const keys = Object.keys(v);
|
|
216
|
-
if (!keys.includes(
|
|
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) +
|
|
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 ===
|
|
245
|
-
process.stdout.write(payload +
|
|
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) +
|
|
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(
|
|
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 ===
|
|
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(
|
|
266
|
+
process.stdout.write('(empty)\n');
|
|
266
267
|
}
|
|
267
268
|
else {
|
|
268
|
-
process.stdout.write(defaultFormat(data) +
|
|
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 ===
|
|
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) +
|
|
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 +
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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 }) +
|
|
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(
|
|
421
|
-
if (typeof info.statement_index ===
|
|
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 ===
|
|
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 +
|
|
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(
|
|
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:
|
|
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
|
+
}
|