@lark-apaas/miaoda-cli 0.1.1 → 0.1.2-alpha.b2b5ae5
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 +25 -0
- package/dist/api/app/index.js +15 -0
- package/dist/api/app/schemas.js +79 -0
- package/dist/api/app/types.js +58 -0
- package/dist/api/db/api.js +83 -6
- package/dist/api/db/client.js +40 -29
- package/dist/api/db/parsers.js +33 -20
- package/dist/api/db/sql-keywords.js +123 -0
- package/dist/api/deploy/api.js +60 -0
- package/dist/api/deploy/index.js +16 -0
- package/dist/api/deploy/schemas.js +103 -0
- package/dist/api/deploy/types.js +22 -0
- package/dist/api/file/api.js +78 -24
- package/dist/api/file/client.js +1 -5
- package/dist/api/file/parsers.js +1 -5
- package/dist/api/index.js +7 -1
- package/dist/api/observability/api.js +52 -0
- package/dist/api/observability/index.js +16 -0
- package/dist/api/observability/schemas.js +39 -0
- package/dist/api/observability/types.js +27 -0
- package/dist/api/plugin/api.js +8 -3
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +1 -0
- package/dist/cli/commands/deploy/index.js +139 -0
- package/dist/cli/commands/index.js +6 -0
- package/dist/cli/commands/observability/index.js +227 -0
- package/dist/cli/commands/plugin/index.js +18 -6
- package/dist/cli/commands/shared.js +38 -6
- package/dist/cli/handlers/app/get.js +48 -0
- package/dist/cli/handlers/app/index.js +7 -0
- package/dist/cli/handlers/app/update.js +59 -0
- package/dist/cli/handlers/db/data.js +22 -2
- package/dist/cli/handlers/db/schema.js +22 -8
- package/dist/cli/handlers/db/sql.js +304 -16
- package/dist/cli/handlers/deploy/deploy.js +83 -0
- package/dist/cli/handlers/deploy/error-log.js +61 -0
- package/dist/cli/handlers/deploy/get.js +70 -0
- package/dist/cli/handlers/deploy/helpers.js +41 -0
- package/dist/cli/handlers/deploy/history.js +70 -0
- package/dist/cli/handlers/deploy/index.js +14 -0
- package/dist/cli/handlers/deploy/polling.js +139 -0
- package/dist/cli/handlers/file/cp.js +39 -17
- package/dist/cli/handlers/file/ls.js +1 -3
- package/dist/cli/handlers/file/rm.js +4 -3
- package/dist/cli/handlers/observability/analytics.js +189 -0
- package/dist/cli/handlers/observability/helpers.js +66 -0
- package/dist/cli/handlers/observability/index.js +12 -0
- package/dist/cli/handlers/observability/log.js +94 -0
- package/dist/cli/handlers/observability/metric.js +208 -0
- package/dist/cli/handlers/observability/trace.js +102 -0
- package/dist/cli/handlers/plugin/plugin-local.js +23 -9
- package/dist/cli/handlers/plugin/plugin.js +21 -7
- package/dist/cli/help.js +5 -2
- package/dist/utils/colors.js +98 -0
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/error.js +11 -0
- package/dist/utils/fuzzy-match.js +91 -0
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +32 -0
- package/dist/utils/index.js +13 -1
- package/dist/utils/output.js +397 -12
- package/dist/utils/render.js +61 -41
- package/dist/utils/time.js +132 -0
- package/package.json +16 -6
package/dist/utils/output.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fmt = void 0;
|
|
3
4
|
exports.isJsonMode = isJsonMode;
|
|
4
5
|
exports.emit = emit;
|
|
5
6
|
exports.emitError = emitError;
|
|
@@ -7,28 +8,379 @@ exports.emitOk = emitOk;
|
|
|
7
8
|
exports.emitPaged = emitPaged;
|
|
8
9
|
const config_1 = require("./config");
|
|
9
10
|
const error_1 = require("./error");
|
|
11
|
+
const colors_1 = require("./colors");
|
|
12
|
+
/**
|
|
13
|
+
* 服务端错误码 → CLI 兜底 hint 文案。
|
|
14
|
+
*
|
|
15
|
+
* 服务端错误协议只有 `{code, msg}` 两个字段(dataloom InnerExecuteSQL 等 IDL
|
|
16
|
+
* 没有 hint / next_actions 通道),所以服务端给的错误本身永远没有 hint。
|
|
17
|
+
* 这里是 CLI 展示层为常见错误码补一份 spec 一致的 actionable 引导,按错误码
|
|
18
|
+
* 落到 next_actions。
|
|
19
|
+
*
|
|
20
|
+
* 仅在 next_actions 已经为空时介入——保留 handler / enrichSqlError 自己塞过的
|
|
21
|
+
* 具体 hint(如 did-you-mean、shared.resolveAppId 等),它们优先级更高。
|
|
22
|
+
*/
|
|
23
|
+
const SERVER_ERROR_HINTS = {
|
|
24
|
+
// SELECT 结果集超过 1000 行硬拦:spec 多行引导。
|
|
25
|
+
// key 用 BIZ_ERR_MAP 映射后的语义 code(不是原始服务端 k_dl_xxx),保持
|
|
26
|
+
// 与 help / 文档里宣传的 RESULT_SET_TOO_LARGE 一致。
|
|
27
|
+
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.",
|
|
30
|
+
],
|
|
31
|
+
};
|
|
10
32
|
function isJsonMode() {
|
|
11
33
|
const cfg = (0, config_1.getConfig)();
|
|
12
34
|
return cfg.output === "json" || Boolean(cfg.json);
|
|
13
35
|
}
|
|
36
|
+
// ── 默认 cell 格式化 ────────────
|
|
37
|
+
/**
|
|
38
|
+
* 单元格的兜底渲染:
|
|
39
|
+
* - null/undefined → ""
|
|
40
|
+
* - string/number/bigint/boolean → toString
|
|
41
|
+
* - 复合值 → JSON.stringify
|
|
42
|
+
* - TTY:超过 60 字符截断(UX 友好)
|
|
43
|
+
* - 非 TTY:完整输出(不破坏管道下游解析)
|
|
44
|
+
*/
|
|
45
|
+
function defaultFormat(v) {
|
|
46
|
+
if (v === null || v === undefined)
|
|
47
|
+
return "";
|
|
48
|
+
if (typeof v === "string")
|
|
49
|
+
return v;
|
|
50
|
+
if (typeof v === "boolean" || typeof v === "number" || typeof v === "bigint")
|
|
51
|
+
return String(v);
|
|
52
|
+
const s = JSON.stringify(v);
|
|
53
|
+
if (process.stdout.isTTY && s.length > 60)
|
|
54
|
+
return s.slice(0, 57) + "...";
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
// ── 时间戳 / 时长 formatter ────────────
|
|
58
|
+
function toMs(v, divisor) {
|
|
59
|
+
let n;
|
|
60
|
+
if (typeof v === "number")
|
|
61
|
+
n = v;
|
|
62
|
+
else if (typeof v === "bigint")
|
|
63
|
+
n = Number(v);
|
|
64
|
+
else if (typeof v === "string") {
|
|
65
|
+
n = Number(v);
|
|
66
|
+
if (!Number.isFinite(n))
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
return null;
|
|
71
|
+
if (!Number.isFinite(n))
|
|
72
|
+
return null;
|
|
73
|
+
return n / divisor;
|
|
74
|
+
}
|
|
75
|
+
/** renderDate 默认模板:本地时区、人读友好。业务层可在 fmt.{ns,us,ms,sec}() 里覆盖。 */
|
|
76
|
+
const DEFAULT_DATE_TEMPLATE = "yyyy-MM-dd HH:mm:ss";
|
|
77
|
+
/**
|
|
78
|
+
* 把 Date 渲染成模板字符串;time zone 走运行时 local。
|
|
79
|
+
* 支持的占位符:yyyy / MM / dd / HH / mm / ss / SSS(毫秒)。
|
|
80
|
+
*/
|
|
81
|
+
function renderDate(date, template = DEFAULT_DATE_TEMPLATE) {
|
|
82
|
+
// if (!process.stdout.isTTY) return date.toISOString();
|
|
83
|
+
// const diffMs = Date.now() - date.getTime();
|
|
84
|
+
// if (diffMs < 0) return date.toISOString();
|
|
85
|
+
// if (diffMs < 60_000) return `${String(Math.floor(diffMs / 1000))}s ago`;
|
|
86
|
+
// if (diffMs < 3_600_000) return `${String(Math.floor(diffMs / 60_000))}m ago`;
|
|
87
|
+
// if (diffMs < 86_400_000) return `${String(Math.floor(diffMs / 3_600_000))}h ago`;
|
|
88
|
+
// 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 Y = String(date.getFullYear());
|
|
91
|
+
const Mo = pad2(date.getMonth() + 1);
|
|
92
|
+
const D = pad2(date.getDate());
|
|
93
|
+
const H = pad2(date.getHours());
|
|
94
|
+
const Mi = pad2(date.getMinutes());
|
|
95
|
+
const S = pad2(date.getSeconds());
|
|
96
|
+
const Ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
97
|
+
// 顺序:先把 SSS 这种长 token 处理掉,再处理短 token,避免子串误覆盖
|
|
98
|
+
return template
|
|
99
|
+
.replace(/yyyy/g, Y)
|
|
100
|
+
.replace(/SSS/g, Ms)
|
|
101
|
+
.replace(/MM/g, Mo)
|
|
102
|
+
.replace(/dd/g, D)
|
|
103
|
+
.replace(/HH/g, H)
|
|
104
|
+
.replace(/mm/g, Mi)
|
|
105
|
+
.replace(/ss/g, S);
|
|
106
|
+
}
|
|
107
|
+
function makeTimestampFormatter(divisor, template) {
|
|
108
|
+
return (v) => {
|
|
109
|
+
const ms = toMs(v, divisor);
|
|
110
|
+
// 0 视作"缺失/哨兵",不渲染为 1970;越界数字也回退原值
|
|
111
|
+
if (ms === null || ms <= 0 || ms > 8.64e15)
|
|
112
|
+
return defaultFormat(v);
|
|
113
|
+
return renderDate(new Date(ms), template);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function formatDurationMs(ms) {
|
|
117
|
+
if (ms < 1)
|
|
118
|
+
return `${ms.toFixed(2)}ms`;
|
|
119
|
+
if (ms < 1000)
|
|
120
|
+
return `${ms.toFixed(0)}ms`;
|
|
121
|
+
if (ms < 60_000)
|
|
122
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
123
|
+
if (ms < 3_600_000)
|
|
124
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
125
|
+
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
126
|
+
}
|
|
127
|
+
function makeDurationFormatter(divisor) {
|
|
128
|
+
return (v) => {
|
|
129
|
+
const ms = toMs(v, divisor);
|
|
130
|
+
if (ms === null || ms < 0)
|
|
131
|
+
return defaultFormat(v);
|
|
132
|
+
return formatDurationMs(ms);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function makeTruncateFormatter(n) {
|
|
136
|
+
return (v) => {
|
|
137
|
+
const s = defaultFormat(v);
|
|
138
|
+
// 仅 TTY 截断,非 TTY 保留完整值(pipe 下游可解析)
|
|
139
|
+
if (!process.stdout.isTTY || s.length <= n)
|
|
140
|
+
return s;
|
|
141
|
+
return s.slice(0, Math.max(0, n - 3)) + "...";
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 内置格式化工厂:handler 在 schema 里按需挑选并按需传模板。
|
|
146
|
+
*
|
|
147
|
+
* 时间戳系列(ns / us / ms / sec)支持可选模板,缺省 "yyyy-MM-dd HH:mm:ss" 本地时区;
|
|
148
|
+
* 时长系列(durationNs / durationUs / durationMs)渲染成 "1.5s" / "200ms" 等可读形式;
|
|
149
|
+
* truncate 是字符截断工厂,参数为最大字符数。
|
|
150
|
+
*
|
|
151
|
+
* 业务层调用方式(注意调用,不是属性引用):
|
|
152
|
+
* { format: fmt.ns() } // 默认模板
|
|
153
|
+
* { format: fmt.ns("yyyy-MM-dd HH:mm:ss.SSS") } // 自定义模板
|
|
154
|
+
* { format: fmt.durationMs() }
|
|
155
|
+
* { format: fmt.truncate(120) }
|
|
156
|
+
*/
|
|
157
|
+
exports.fmt = {
|
|
158
|
+
/** 纳秒时间戳;可选模板 */
|
|
159
|
+
ns: (template) => makeTimestampFormatter(1e6, template),
|
|
160
|
+
/** 微秒时间戳;可选模板 */
|
|
161
|
+
us: (template) => makeTimestampFormatter(1e3, template),
|
|
162
|
+
/** 毫秒时间戳;可选模板 */
|
|
163
|
+
ms: (template) => makeTimestampFormatter(1, template),
|
|
164
|
+
/** 秒时间戳;可选模板 */
|
|
165
|
+
sec: (template) => makeTimestampFormatter(1 / 1000, template),
|
|
166
|
+
/** 纳秒时长 → "1.5s" / "200ms" 等可读形式 */
|
|
167
|
+
durationNs: () => makeDurationFormatter(1e6),
|
|
168
|
+
/** 微秒时长 */
|
|
169
|
+
durationUs: () => makeDurationFormatter(1e3),
|
|
170
|
+
/** 毫秒时长 */
|
|
171
|
+
durationMs: () => makeDurationFormatter(1),
|
|
172
|
+
/** 显式截断(仅 TTY 截断,pipe 保留完整值) */
|
|
173
|
+
truncate: makeTruncateFormatter,
|
|
174
|
+
};
|
|
175
|
+
// ── 字段选择 (--json field1,field2) ────────────
|
|
176
|
+
function getFieldSelection() {
|
|
177
|
+
const cfg = (0, config_1.getConfig)();
|
|
178
|
+
if (typeof cfg.json !== "string")
|
|
179
|
+
return null;
|
|
180
|
+
const fields = cfg.json
|
|
181
|
+
.split(",")
|
|
182
|
+
.map((s) => s.trim())
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
return fields.length > 0 ? fields : null;
|
|
185
|
+
}
|
|
186
|
+
function pickFields(obj, fields) {
|
|
187
|
+
const out = {};
|
|
188
|
+
let matched = false;
|
|
189
|
+
for (const f of fields) {
|
|
190
|
+
if (f in obj) {
|
|
191
|
+
out[f] = obj[f];
|
|
192
|
+
matched = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// fail-open:异构 row(如 db sql 多语句的 INSERT 元素)一个 field 都没命中时
|
|
196
|
+
// 保留原 row,避免 INSERT 元数据被裁成空对象
|
|
197
|
+
return matched ? out : obj;
|
|
198
|
+
}
|
|
199
|
+
function applyFieldSelection(data, fields) {
|
|
200
|
+
if (Array.isArray(data)) {
|
|
201
|
+
return data.map((item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
202
|
+
? pickFields(item, fields)
|
|
203
|
+
: item);
|
|
204
|
+
}
|
|
205
|
+
if (typeof data === "object" && data !== null) {
|
|
206
|
+
return pickFields(data, fields);
|
|
207
|
+
}
|
|
208
|
+
return data;
|
|
209
|
+
}
|
|
210
|
+
// ── 信封识别 ────────────
|
|
211
|
+
const ENVELOPE_KEYS = new Set(["data", "next_cursor", "has_more"]);
|
|
212
|
+
function isEnvelope(v) {
|
|
213
|
+
if (typeof v !== "object" || v === null)
|
|
214
|
+
return false;
|
|
215
|
+
const keys = Object.keys(v);
|
|
216
|
+
if (!keys.includes("data"))
|
|
217
|
+
return false;
|
|
218
|
+
return keys.every((k) => ENVELOPE_KEYS.has(k));
|
|
219
|
+
}
|
|
220
|
+
// ── emit ────────────
|
|
14
221
|
/**
|
|
15
222
|
* 输出数据
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
223
|
+
* - JSON 模式:单行紧凑 JSON,**原样透传**(schema 不参与),仅 `--json a,b`
|
|
224
|
+
* 字段筛选会生效。这样下游消费者拿到的 JSON 形状与 BAM 接口契约一致。
|
|
225
|
+
* - Pretty 模式:信封 array → 表格;信封 object → key-value;非信封 → 缩进 JSON
|
|
226
|
+
* - 有 schema 时:列顺序、标签、格式、derive 由 schema.columns 决定;
|
|
227
|
+
* strict=false 时未声明字段在尾部追加
|
|
228
|
+
* - 无 schema 时:时间戳字段按命名约定(*timestampNs/Us/Ms/sec)兜底渲染
|
|
18
229
|
*/
|
|
19
|
-
function emit(data) {
|
|
230
|
+
function emit(data, schema) {
|
|
231
|
+
let payload = data;
|
|
232
|
+
const fields = getFieldSelection();
|
|
233
|
+
if (fields && isEnvelope(payload)) {
|
|
234
|
+
payload = { ...payload, data: applyFieldSelection(payload.data, fields) };
|
|
235
|
+
}
|
|
20
236
|
if (isJsonMode()) {
|
|
21
|
-
process.stdout.write(JSON.stringify(
|
|
237
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (isEnvelope(payload)) {
|
|
241
|
+
emitPrettyEnvelope(payload, schema);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (typeof payload === "string") {
|
|
245
|
+
process.stdout.write(payload + "\n");
|
|
22
246
|
}
|
|
23
247
|
else {
|
|
24
|
-
|
|
25
|
-
|
|
248
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function emitPrettyEnvelope(env, schema) {
|
|
252
|
+
const { data, next_cursor, has_more } = env;
|
|
253
|
+
if (Array.isArray(data)) {
|
|
254
|
+
if (data.length === 0) {
|
|
255
|
+
process.stdout.write("(no results)\n");
|
|
26
256
|
}
|
|
27
257
|
else {
|
|
28
|
-
|
|
258
|
+
writeTable(data, schema);
|
|
29
259
|
}
|
|
30
260
|
}
|
|
261
|
+
else if (data !== null && typeof data === "object") {
|
|
262
|
+
writeKeyValue(data, schema);
|
|
263
|
+
}
|
|
264
|
+
else if (data === null || data === undefined) {
|
|
265
|
+
process.stdout.write("(empty)\n");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
process.stdout.write(defaultFormat(data) + "\n");
|
|
269
|
+
}
|
|
270
|
+
if (has_more && next_cursor) {
|
|
271
|
+
const count = Array.isArray(data) ? data.length : 1;
|
|
272
|
+
process.stdout.write(`\n— ${String(count)} results. Next: --cursor ${next_cursor}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function resolveColumnsFromSchema(schema) {
|
|
276
|
+
const cols = [];
|
|
277
|
+
const declared = new Set();
|
|
278
|
+
for (const col of schema.columns) {
|
|
279
|
+
declared.add(col.key);
|
|
280
|
+
cols.push({
|
|
281
|
+
key: col.key,
|
|
282
|
+
label: col.label ?? col.key,
|
|
283
|
+
format: col.format ?? defaultFormat,
|
|
284
|
+
derive: col.derive,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return { cols, declared };
|
|
288
|
+
}
|
|
289
|
+
function readCell(row, col) {
|
|
290
|
+
return col.derive ? col.derive(row) : row[col.key];
|
|
291
|
+
}
|
|
292
|
+
function resolveColumnsForRows(items, schema) {
|
|
293
|
+
if (schema) {
|
|
294
|
+
const { cols, declared } = resolveColumnsFromSchema(schema);
|
|
295
|
+
if (!schema.strict) {
|
|
296
|
+
for (const item of items) {
|
|
297
|
+
for (const k of Object.keys(item)) {
|
|
298
|
+
if (!declared.has(k)) {
|
|
299
|
+
declared.add(k);
|
|
300
|
+
cols.push({ key: k, label: k, format: defaultFormat });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return cols;
|
|
306
|
+
}
|
|
307
|
+
// 无 schema:列顺序按首次出现顺序,全部走 defaultFormat
|
|
308
|
+
const cols = [];
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
for (const item of items) {
|
|
311
|
+
for (const k of Object.keys(item)) {
|
|
312
|
+
if (!seen.has(k)) {
|
|
313
|
+
seen.add(k);
|
|
314
|
+
cols.push({ key: k, label: k, format: defaultFormat });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return cols;
|
|
31
319
|
}
|
|
320
|
+
function writeTable(rows, schema) {
|
|
321
|
+
const items = rows.filter((r) => typeof r === "object" && r !== null && !Array.isArray(r));
|
|
322
|
+
if (items.length === 0) {
|
|
323
|
+
for (const row of rows)
|
|
324
|
+
process.stdout.write(defaultFormat(row) + "\n");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const cols = resolveColumnsForRows(items, schema);
|
|
328
|
+
const matrix = [
|
|
329
|
+
cols.map((c) => c.label),
|
|
330
|
+
...items.map((item) => cols.map((c) => c.format(readCell(item, c)))),
|
|
331
|
+
];
|
|
332
|
+
if (process.stdout.isTTY) {
|
|
333
|
+
const widths = cols.map((_, i) => Math.max(...matrix.map((row) => row[i]?.length ?? 0)));
|
|
334
|
+
for (const row of matrix) {
|
|
335
|
+
const line = row
|
|
336
|
+
.map((cell, i) => cell.padEnd(widths[i] ?? 0))
|
|
337
|
+
.join(" ")
|
|
338
|
+
.trimEnd();
|
|
339
|
+
process.stdout.write(line + "\n");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
for (const row of matrix)
|
|
344
|
+
process.stdout.write(row.join("\t") + "\n");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function resolveColumnsForObject(obj, schema) {
|
|
348
|
+
if (schema) {
|
|
349
|
+
const { cols, declared } = resolveColumnsFromSchema(schema);
|
|
350
|
+
if (!schema.strict) {
|
|
351
|
+
for (const k of Object.keys(obj)) {
|
|
352
|
+
if (!declared.has(k)) {
|
|
353
|
+
cols.push({ key: k, label: k, format: defaultFormat });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return cols;
|
|
358
|
+
}
|
|
359
|
+
return Object.keys(obj).map((k) => ({
|
|
360
|
+
key: k,
|
|
361
|
+
label: k,
|
|
362
|
+
format: defaultFormat,
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
function writeKeyValue(obj, schema) {
|
|
366
|
+
const cols = resolveColumnsForObject(obj, schema);
|
|
367
|
+
if (cols.length === 0) {
|
|
368
|
+
process.stdout.write("(empty)\n");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (process.stdout.isTTY) {
|
|
372
|
+
const maxKey = Math.max(...cols.map((c) => c.label.length));
|
|
373
|
+
for (const c of cols) {
|
|
374
|
+
process.stdout.write(`${c.label.padStart(maxKey)}: ${c.format(readCell(obj, c))}\n`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
for (const c of cols) {
|
|
379
|
+
process.stdout.write(`${c.label}\t${c.format(readCell(obj, c))}\n`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ── 错误输出 ────────────
|
|
32
384
|
/**
|
|
33
385
|
* 输出错误(写入 stderr)
|
|
34
386
|
* - pretty 模式:Error: message\n hint: ...
|
|
@@ -36,23 +388,56 @@ function emit(data) {
|
|
|
36
388
|
*/
|
|
37
389
|
function emitError(err) {
|
|
38
390
|
const info = toErrorInfo(err);
|
|
391
|
+
// 没人给过 next_actions 才走错误码兜底;handler 已经塞过具体 hint 时不覆盖
|
|
392
|
+
const hints = info.next_actions && info.next_actions.length > 0
|
|
393
|
+
? info.next_actions
|
|
394
|
+
: (SERVER_ERROR_HINTS[info.code] ?? []);
|
|
39
395
|
if (isJsonMode()) {
|
|
40
396
|
const errObj = {
|
|
41
397
|
code: info.code,
|
|
42
398
|
message: info.message,
|
|
43
399
|
};
|
|
44
|
-
if (
|
|
45
|
-
|
|
400
|
+
if (hints.length > 0) {
|
|
401
|
+
// JSON 输出压平成单行,更便于机器消费(脚本 / agent 拼字符串)
|
|
402
|
+
errObj.hint = hints.join(" ");
|
|
46
403
|
}
|
|
47
404
|
if (typeof info.statement_index === "number") {
|
|
48
405
|
errObj.statement_index = info.statement_index;
|
|
49
406
|
}
|
|
407
|
+
// PRD 多语句失败 envelope 额外字段:completed / rolled_back
|
|
408
|
+
if (Array.isArray(info.completed)) {
|
|
409
|
+
errObj.completed = info.completed;
|
|
410
|
+
}
|
|
411
|
+
if (typeof info.rolled_back === "boolean") {
|
|
412
|
+
errObj.rolled_back = info.rolled_back;
|
|
413
|
+
}
|
|
50
414
|
process.stderr.write(JSON.stringify({ error: errObj }) + "\n");
|
|
51
415
|
}
|
|
52
416
|
else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
417
|
+
// stderr 染色:picocolors 默认按 stdout.isTTY 判断;stderr 通常也是 tty,
|
|
418
|
+
// 这里复用 stdout 探测保持简单(stderr only-pipe 的极端场景留作后续优化)
|
|
419
|
+
// 多语句失败时 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") {
|
|
422
|
+
const k = info.statement_index + 1;
|
|
423
|
+
const n = info.total_statements;
|
|
424
|
+
errorLine +=
|
|
425
|
+
typeof n === "number" && n > 0
|
|
426
|
+
? ` (at statement ${String(k)} of ${String(n)})`
|
|
427
|
+
: ` (at statement ${String(k)})`;
|
|
428
|
+
}
|
|
429
|
+
process.stderr.write(errorLine + "\n");
|
|
430
|
+
// 多行 hint:第一行带 "hint:" 标签,后续行用 8 空格缩进对齐 " hint: " 之后的列。
|
|
431
|
+
// 对应 spec 期望的格式:
|
|
432
|
+
// Error: ...
|
|
433
|
+
// hint: 第一条建议
|
|
434
|
+
// 第二条建议
|
|
435
|
+
if (hints.length > 0) {
|
|
436
|
+
const [first, ...rest] = hints;
|
|
437
|
+
process.stderr.write(` ${colors_1.c.muted("hint:")} ${first}\n`);
|
|
438
|
+
for (const line of rest) {
|
|
439
|
+
process.stderr.write(` ${line}\n`);
|
|
440
|
+
}
|
|
56
441
|
}
|
|
57
442
|
}
|
|
58
443
|
}
|
package/dist/utils/render.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSize = formatSize;
|
|
4
|
+
exports.formatTime = formatTime;
|
|
5
|
+
exports.visibleWidth = visibleWidth;
|
|
6
|
+
exports.renderAlignedTable = renderAlignedTable;
|
|
7
|
+
exports.renderTsv = renderTsv;
|
|
8
|
+
exports.renderKeyValue = renderKeyValue;
|
|
9
|
+
exports.isStdoutTty = isStdoutTty;
|
|
10
|
+
exports.parseDuration = parseDuration;
|
|
11
|
+
exports.parseSize = parseSize;
|
|
2
12
|
/**
|
|
3
13
|
* CLI 渲染 / 解析工具:跨域共用的格式化、表格渲染、字符串解析。
|
|
4
14
|
*
|
|
@@ -9,18 +19,12 @@
|
|
|
9
19
|
* - 终端探测:isStdoutTty
|
|
10
20
|
* - 字符串解析:parseDuration / parseSize
|
|
11
21
|
*
|
|
22
|
+
* 彩色高亮的语义层封装见 ./colors.ts。表头 / key 标签等结构性元素由本文件
|
|
23
|
+
* 主动调用 colors.c 染色;业务文案的染色(成功/失败 prefix 等)由 handler 自治。
|
|
24
|
+
*
|
|
12
25
|
* JSON envelope 输出(emit / emitOk / emitPaged / emitError)见 ./output.ts。
|
|
13
26
|
*/
|
|
14
|
-
|
|
15
|
-
exports.formatSize = formatSize;
|
|
16
|
-
exports.formatTime = formatTime;
|
|
17
|
-
exports.visibleWidth = visibleWidth;
|
|
18
|
-
exports.renderAlignedTable = renderAlignedTable;
|
|
19
|
-
exports.renderTsv = renderTsv;
|
|
20
|
-
exports.renderKeyValue = renderKeyValue;
|
|
21
|
-
exports.isStdoutTty = isStdoutTty;
|
|
22
|
-
exports.parseDuration = parseDuration;
|
|
23
|
-
exports.parseSize = parseSize;
|
|
27
|
+
const colors_1 = require("./colors");
|
|
24
28
|
/** 将字节数格式化为人类可读(`24 KB` / `2.1 MB` / `1.5 GB`)。 */
|
|
25
29
|
function formatSize(bytes) {
|
|
26
30
|
if (!Number.isFinite(bytes) || bytes < 0)
|
|
@@ -77,20 +81,21 @@ const ANSI_SGR_RE = /\[[0-9;]*m/g;
|
|
|
77
81
|
* 不实现合字 / 零宽字符(ZWJ / 变体选择符)等极端情况,CLI 表格场景够用。
|
|
78
82
|
*/
|
|
79
83
|
function charWidth(cp) {
|
|
80
|
-
if ((cp >= 0x1100 && cp <=
|
|
81
|
-
cp === 0x2329 ||
|
|
82
|
-
|
|
83
|
-
(cp >=
|
|
84
|
-
(cp >=
|
|
85
|
-
(cp >=
|
|
86
|
-
(cp >=
|
|
87
|
-
(cp >=
|
|
88
|
-
(cp >=
|
|
89
|
-
(cp >=
|
|
90
|
-
(cp >=
|
|
91
|
-
(cp >=
|
|
92
|
-
(cp >=
|
|
93
|
-
(cp >=
|
|
84
|
+
if ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
85
|
+
cp === 0x2329 ||
|
|
86
|
+
cp === 0x232a ||
|
|
87
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals / Punctuation
|
|
88
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana / Katakana / CJK Symbols
|
|
89
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
|
|
90
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
|
|
91
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
92
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
|
|
93
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compat Ideographs
|
|
94
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK Compat Forms
|
|
95
|
+
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
|
|
96
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
97
|
+
(cp >= 0x20000 && cp <= 0x2fffd) || // CJK Ext B-F
|
|
98
|
+
(cp >= 0x30000 && cp <= 0x3fffd) // CJK Ext G-H
|
|
94
99
|
) {
|
|
95
100
|
return 2;
|
|
96
101
|
}
|
|
@@ -109,7 +114,8 @@ function padVisibleEnd(s, targetWidth) {
|
|
|
109
114
|
const w = visibleWidth(s);
|
|
110
115
|
return w >= targetWidth ? s : s + " ".repeat(targetWidth - w);
|
|
111
116
|
}
|
|
112
|
-
/** 渲染 TTY 对齐表格(多行,列宽按最长内容;ANSI 转义不计入列宽)。
|
|
117
|
+
/** 渲染 TTY 对齐表格(多行,列宽按最长内容;ANSI 转义不计入列宽)。
|
|
118
|
+
* 表头按 spec 用 bold + cyan 染色;ANSI 序列由 visibleWidth 剥离不影响列宽。 */
|
|
113
119
|
function renderAlignedTable(headers, rows) {
|
|
114
120
|
const colWidths = headers.map((h, i) => {
|
|
115
121
|
let w = visibleWidth(h);
|
|
@@ -121,9 +127,15 @@ function renderAlignedTable(headers, rows) {
|
|
|
121
127
|
return w;
|
|
122
128
|
});
|
|
123
129
|
const lines = [];
|
|
124
|
-
lines.push(headers
|
|
130
|
+
lines.push(headers
|
|
131
|
+
.map((h, i) => colors_1.c.header(padVisibleEnd(h, colWidths[i])))
|
|
132
|
+
.join(" ")
|
|
133
|
+
.trimEnd());
|
|
125
134
|
for (const row of rows) {
|
|
126
|
-
lines.push(row
|
|
135
|
+
lines.push(row
|
|
136
|
+
.map((cell, i) => padVisibleEnd(cell || "", colWidths[i]))
|
|
137
|
+
.join(" ")
|
|
138
|
+
.trimEnd());
|
|
127
139
|
}
|
|
128
140
|
return lines.join("\n");
|
|
129
141
|
}
|
|
@@ -136,7 +148,7 @@ function renderTsv(headers, rows) {
|
|
|
136
148
|
}
|
|
137
149
|
return lines.join("\n");
|
|
138
150
|
}
|
|
139
|
-
/** 渲染 key-value 多行(用于 stat 等单条详情)。key
|
|
151
|
+
/** 渲染 key-value 多行(用于 stat 等单条详情)。key 右对齐 + bold cyan 染色。 */
|
|
140
152
|
function renderKeyValue(pairs, isTty) {
|
|
141
153
|
if (pairs.length === 0)
|
|
142
154
|
return "";
|
|
@@ -144,9 +156,7 @@ function renderKeyValue(pairs, isTty) {
|
|
|
144
156
|
return pairs.map(([k, v]) => `${k}\t${v}`).join("\n");
|
|
145
157
|
}
|
|
146
158
|
const keyWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
147
|
-
return pairs
|
|
148
|
-
.map(([k, v]) => `${k.padStart(keyWidth)}: ${v}`)
|
|
149
|
-
.join("\n");
|
|
159
|
+
return pairs.map(([k, v]) => `${colors_1.c.header(k.padStart(keyWidth))}: ${v}`).join("\n");
|
|
150
160
|
}
|
|
151
161
|
/** 通用 isTTY 判定(stdout 是否交互终端)。Node 运行时 isTTY 为 true 或 undefined;TS 类型上 tty.WriteStream 定义为固定 true,绕开做运行时判断。 */
|
|
152
162
|
function isStdoutTty() {
|
|
@@ -162,11 +172,16 @@ function parseDuration(input) {
|
|
|
162
172
|
const n = Number(m[1]);
|
|
163
173
|
const unit = m[2] || "s";
|
|
164
174
|
switch (unit) {
|
|
165
|
-
case "s":
|
|
166
|
-
|
|
167
|
-
case "
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
case "s":
|
|
176
|
+
return n;
|
|
177
|
+
case "m":
|
|
178
|
+
return n * 60;
|
|
179
|
+
case "h":
|
|
180
|
+
return n * 3600;
|
|
181
|
+
case "d":
|
|
182
|
+
return n * 86400;
|
|
183
|
+
default:
|
|
184
|
+
return n;
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
/** 解析 size 字符串 `1MB` / `500KB` / `1GB` → 字节。 */
|
|
@@ -178,10 +193,15 @@ function parseSize(input) {
|
|
|
178
193
|
const n = Number(m[1]);
|
|
179
194
|
const unit = (m[2] || "B").toUpperCase();
|
|
180
195
|
switch (unit) {
|
|
181
|
-
case "B":
|
|
182
|
-
|
|
183
|
-
case "
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
case "B":
|
|
197
|
+
return Math.round(n);
|
|
198
|
+
case "KB":
|
|
199
|
+
return Math.round(n * 1024);
|
|
200
|
+
case "MB":
|
|
201
|
+
return Math.round(n * 1024 * 1024);
|
|
202
|
+
case "GB":
|
|
203
|
+
return Math.round(n * 1024 * 1024 * 1024);
|
|
204
|
+
default:
|
|
205
|
+
return Math.round(n);
|
|
186
206
|
}
|
|
187
207
|
}
|