@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.
- 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/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 +12 -12
- package/dist/cli/commands/db/index.js +602 -54
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/file/index.js +85 -58
- 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/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/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/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 +19 -19
- package/dist/utils/index.js +3 -1
- package/dist/utils/output.js +67 -45
- package/dist/utils/poll.js +35 -0
- package/dist/utils/render.js +27 -27
- package/dist/utils/spinner.js +46 -0
- package/dist/utils/time.js +47 -42
- package/package.json +1 -1
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
|
+
}
|
package/dist/utils/render.js
CHANGED
|
@@ -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,
|
|
69
|
-
const da = String(date.getDate()).padStart(2,
|
|
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 +
|
|
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 ||
|
|
137
|
-
.join(
|
|
136
|
+
.map((cell, i) => padVisibleEnd(cell || '', colWidths[i]))
|
|
137
|
+
.join(' ')
|
|
138
138
|
.trimEnd());
|
|
139
139
|
}
|
|
140
|
-
return lines.join(
|
|
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(
|
|
145
|
+
lines.push(headers.join('\t'));
|
|
146
146
|
for (const row of rows) {
|
|
147
|
-
lines.push(row.join(
|
|
147
|
+
lines.push(row.join('\t'));
|
|
148
148
|
}
|
|
149
|
-
return lines.join(
|
|
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(
|
|
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(
|
|
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] ||
|
|
173
|
+
const unit = m[2] || 's';
|
|
174
174
|
switch (unit) {
|
|
175
|
-
case
|
|
175
|
+
case 's':
|
|
176
176
|
return n;
|
|
177
|
-
case
|
|
177
|
+
case 'm':
|
|
178
178
|
return n * 60;
|
|
179
|
-
case
|
|
179
|
+
case 'h':
|
|
180
180
|
return n * 3600;
|
|
181
|
-
case
|
|
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] ||
|
|
194
|
+
const unit = (m[2] || 'B').toUpperCase();
|
|
195
195
|
switch (unit) {
|
|
196
|
-
case
|
|
196
|
+
case 'B':
|
|
197
197
|
return Math.round(n);
|
|
198
|
-
case
|
|
198
|
+
case 'KB':
|
|
199
199
|
return Math.round(n * 1024);
|
|
200
|
-
case
|
|
200
|
+
case 'MB':
|
|
201
201
|
return Math.round(n * 1024 * 1024);
|
|
202
|
-
case
|
|
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[K';
|
|
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
|
+
}
|