@lark-apaas/miaoda-cli 0.1.2 → 0.1.3
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/README.md +8 -7
- 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/deploy/api.js +60 -0
- package/dist/api/deploy/index.js +16 -0
- package/dist/api/deploy/schemas.js +105 -0
- package/dist/api/deploy/types.js +22 -0
- 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 +60 -0
- package/dist/api/observability/types.js +27 -0
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +6 -5
- package/dist/cli/commands/deploy/index.js +155 -0
- package/dist/cli/commands/file/index.js +6 -5
- package/dist/cli/commands/index.js +10 -6
- package/dist/cli/commands/observability/index.js +240 -0
- package/dist/cli/commands/shared.js +83 -7
- package/dist/cli/handlers/app/get.js +47 -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 +2 -3
- package/dist/cli/handlers/db/schema.js +2 -3
- package/dist/cli/handlers/db/sql.js +1 -2
- package/dist/cli/handlers/deploy/deploy.js +84 -0
- package/dist/cli/handlers/deploy/error-log.js +60 -0
- package/dist/cli/handlers/deploy/format.js +39 -0
- package/dist/cli/handlers/deploy/get.js +71 -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 +162 -0
- package/dist/cli/handlers/file/cp.js +1 -2
- package/dist/cli/handlers/file/ls.js +1 -2
- package/dist/cli/handlers/file/rm.js +1 -2
- package/dist/cli/handlers/file/sign.js +1 -2
- package/dist/cli/handlers/file/stat.js +1 -2
- package/dist/cli/handlers/observability/analytics.js +212 -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/main.js +6 -2
- package/dist/utils/args.js +8 -0
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +118 -0
- package/dist/utils/index.js +13 -1
- package/dist/utils/output.js +338 -7
- package/dist/utils/time.js +203 -0
- package/package.json +7 -5
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;
|
|
@@ -32,24 +33,354 @@ function isJsonMode() {
|
|
|
32
33
|
const cfg = (0, config_1.getConfig)();
|
|
33
34
|
return cfg.output === "json" || Boolean(cfg.json);
|
|
34
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 ────────────
|
|
35
221
|
/**
|
|
36
222
|
* 输出数据
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
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)兜底渲染
|
|
39
229
|
*/
|
|
40
|
-
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
|
+
}
|
|
41
236
|
if (isJsonMode()) {
|
|
42
|
-
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");
|
|
43
246
|
}
|
|
44
247
|
else {
|
|
45
|
-
|
|
46
|
-
|
|
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");
|
|
47
256
|
}
|
|
48
257
|
else {
|
|
49
|
-
|
|
258
|
+
writeTable(data, schema);
|
|
259
|
+
}
|
|
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;
|
|
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`);
|
|
50
380
|
}
|
|
51
381
|
}
|
|
52
382
|
}
|
|
383
|
+
// ── 错误输出 ────────────
|
|
53
384
|
/**
|
|
54
385
|
* 输出错误(写入 stderr)
|
|
55
386
|
* - pretty 模式:Error: message\n hint: ...
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TIMESTAMP_HELP = void 0;
|
|
4
|
+
exports.parseTimeToMs = parseTimeToMs;
|
|
5
|
+
exports.msToNs = msToNs;
|
|
6
|
+
exports.msToSec = msToSec;
|
|
7
|
+
exports.parseToMs = parseToMs;
|
|
8
|
+
exports.parseToNs = parseToNs;
|
|
9
|
+
exports.parseToSec = parseToSec;
|
|
10
|
+
exports.floorMsToBucket = floorMsToBucket;
|
|
11
|
+
exports.ceilMsToBucket = ceilMsToBucket;
|
|
12
|
+
const error_1 = require("./error");
|
|
13
|
+
exports.TIMESTAMP_HELP = "支持格式:" +
|
|
14
|
+
"相对时间 30m/1h/2d/1w;" +
|
|
15
|
+
"日期 2026-04-01(本地时区当日 00:00:00);" +
|
|
16
|
+
"本地日期+时间 2026-04-01T10:00:00(按本地时区,T 分隔);" +
|
|
17
|
+
"带时区 ISO 2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)。" +
|
|
18
|
+
"禁止用空格分隔:'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数。";
|
|
19
|
+
/**
|
|
20
|
+
* 解析时间字符串到毫秒时间戳;不匹配任一支持格式抛 ARGS_INVALID。
|
|
21
|
+
*
|
|
22
|
+
* 时区约定(重要):
|
|
23
|
+
* - 不带显式时区的形式(YYYY-MM-DD、YYYY-MM-DDTHH:mm:ss)一律按
|
|
24
|
+
* **本地时区**解释,与 pretty 输出(output.ts:renderDate 用 getFullYear 等本地方法)形成
|
|
25
|
+
* 输入/输出闭环:用户复制输出文本作为 --since 不会差时区。
|
|
26
|
+
* - 带显式时区(结尾 Z 或 ±HH:MM / ±HHMM)按显式时区解析,跨机器一致。
|
|
27
|
+
* - 因此跨机器同步使用时建议带显式时区;同机器复制粘贴 pretty 输出更省事。
|
|
28
|
+
*
|
|
29
|
+
* 拒绝 Date.parse 松散接受的形式:YYYY/MM/DD、自然语言('April 1 2026')、单独年份等。
|
|
30
|
+
*
|
|
31
|
+
* 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
|
|
32
|
+
*/
|
|
33
|
+
function parseTimeToMs(input, now = new Date()) {
|
|
34
|
+
// 1. 相对时间:30m / 1h / 2d / 1w
|
|
35
|
+
const relative = /^(\d+)([mhdw])$/.exec(input);
|
|
36
|
+
if (relative) {
|
|
37
|
+
const n = Number(relative[1]);
|
|
38
|
+
const unit = relative[2];
|
|
39
|
+
const factor = unit === "m"
|
|
40
|
+
? 60_000
|
|
41
|
+
: unit === "h"
|
|
42
|
+
? 3_600_000
|
|
43
|
+
: unit === "d"
|
|
44
|
+
? 86_400_000
|
|
45
|
+
: /* w */ 604_800_000;
|
|
46
|
+
return now.getTime() - n * factor;
|
|
47
|
+
}
|
|
48
|
+
// 2. 纯日期:YYYY-MM-DD → 本地当日 00:00:00
|
|
49
|
+
const date = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
|
|
50
|
+
if (date) {
|
|
51
|
+
return localDateMs(input, date[1], date[2], date[3], "00", "00", "00", "0");
|
|
52
|
+
}
|
|
53
|
+
// 3. 本地日期+时间:YYYY-MM-DDTHH:mm:ss[.SSS](不带时区,T 分隔)。
|
|
54
|
+
// 不接受空格分隔——'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数,
|
|
55
|
+
// 宁可不支持也不要让 agent 踩"看起来传了 since 实际只传了一半"的坑。
|
|
56
|
+
const localDt = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(input);
|
|
57
|
+
if (localDt) {
|
|
58
|
+
// capture 7 是可选 .SSS 组,运行时可能 undefined;TS lib 的 RegExp match 索引
|
|
59
|
+
// 类型把它标成 string,所以这里显式 cast 让 ?? 在 lint 视角下也"必要"。
|
|
60
|
+
const msPart = localDt[7] ?? "0";
|
|
61
|
+
return localDateMs(input, localDt[1], localDt[2], localDt[3], localDt[4], localDt[5], localDt[6], msPart);
|
|
62
|
+
}
|
|
63
|
+
// 4. 带显式时区的 ISO 8601:YYYY-MM-DDTHH:mm:ss[.SSS](Z|±HH:MM|±HHMM)
|
|
64
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-](\d{2}):?(\d{2}))$/.exec(input);
|
|
65
|
+
if (iso) {
|
|
66
|
+
validateDateTimeParts(input, iso[1], iso[2], iso[3], iso[4], iso[5], iso[6]);
|
|
67
|
+
if (iso[7] !== "Z")
|
|
68
|
+
validateOffset(input, iso[8], iso[9]);
|
|
69
|
+
const ms = Date.parse(input);
|
|
70
|
+
if (Number.isNaN(ms))
|
|
71
|
+
failInvalidTimestamp(input);
|
|
72
|
+
return ms;
|
|
73
|
+
}
|
|
74
|
+
failInvalidTimestamp(input);
|
|
75
|
+
}
|
|
76
|
+
function localDateMs(input, y, mo, d, h, mi, s, msPart) {
|
|
77
|
+
validateDateTimeParts(input, y, mo, d, h, mi, s);
|
|
78
|
+
// 毫秒位补齐到 3 位再截断("5" → 500,"12" → 120,"123" → 123)
|
|
79
|
+
const ms = Number(msPart.padEnd(3, "0").slice(0, 3));
|
|
80
|
+
const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s), ms);
|
|
81
|
+
if (date.getFullYear() !== Number(y) ||
|
|
82
|
+
date.getMonth() !== Number(mo) - 1 ||
|
|
83
|
+
date.getDate() !== Number(d) ||
|
|
84
|
+
date.getHours() !== Number(h) ||
|
|
85
|
+
date.getMinutes() !== Number(mi) ||
|
|
86
|
+
date.getSeconds() !== Number(s) ||
|
|
87
|
+
date.getMilliseconds() !== ms) {
|
|
88
|
+
failInvalidTimestamp(input);
|
|
89
|
+
}
|
|
90
|
+
return date.getTime();
|
|
91
|
+
}
|
|
92
|
+
function validateDateTimeParts(input, y, mo, d, h, mi, s) {
|
|
93
|
+
const year = Number(y);
|
|
94
|
+
const month = Number(mo);
|
|
95
|
+
const day = Number(d);
|
|
96
|
+
const hour = Number(h);
|
|
97
|
+
const minute = Number(mi);
|
|
98
|
+
const second = Number(s);
|
|
99
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) {
|
|
100
|
+
failInvalidTimestamp(input);
|
|
101
|
+
}
|
|
102
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
103
|
+
if (utc.getUTCFullYear() !== year ||
|
|
104
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
105
|
+
utc.getUTCDate() !== day) {
|
|
106
|
+
failInvalidTimestamp(input);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateOffset(input, h, mi) {
|
|
110
|
+
if (h === undefined || mi === undefined)
|
|
111
|
+
failInvalidTimestamp(input);
|
|
112
|
+
const hour = Number(h);
|
|
113
|
+
const minute = Number(mi);
|
|
114
|
+
if (hour > 23 || minute > 59)
|
|
115
|
+
failInvalidTimestamp(input);
|
|
116
|
+
}
|
|
117
|
+
function failInvalidTimestamp(input) {
|
|
118
|
+
throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${exports.TIMESTAMP_HELP}`);
|
|
119
|
+
}
|
|
120
|
+
/** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
|
|
121
|
+
function msToNs(ms) {
|
|
122
|
+
return `${String(ms)}000000`;
|
|
123
|
+
}
|
|
124
|
+
/** 毫秒 → 秒(字符串,向下取整) */
|
|
125
|
+
function msToSec(ms) {
|
|
126
|
+
return String(Math.floor(ms / 1000));
|
|
127
|
+
}
|
|
128
|
+
/** 解析到毫秒数;input 为空返回 undefined */
|
|
129
|
+
function parseToMs(input, now) {
|
|
130
|
+
if (!input)
|
|
131
|
+
return undefined;
|
|
132
|
+
return parseTimeToMs(input, now);
|
|
133
|
+
}
|
|
134
|
+
/** 解析到纳秒字符串;input 为空返回 undefined */
|
|
135
|
+
function parseToNs(input, now) {
|
|
136
|
+
if (!input)
|
|
137
|
+
return undefined;
|
|
138
|
+
return msToNs(parseTimeToMs(input, now));
|
|
139
|
+
}
|
|
140
|
+
/** 解析到秒字符串;input 为空返回 undefined */
|
|
141
|
+
function parseToSec(input, now) {
|
|
142
|
+
if (!input)
|
|
143
|
+
return undefined;
|
|
144
|
+
return msToSec(parseTimeToMs(input, now));
|
|
145
|
+
}
|
|
146
|
+
// ── 桶边界对齐 ──
|
|
147
|
+
//
|
|
148
|
+
// metric 的 down-sample 与 analytics 的 timeAggregationUnit 都按 UTC 桶切片。
|
|
149
|
+
// 当用户传的 since/until 落在桶中间,服务端可能返回不到该桶(边界数据丢失)。
|
|
150
|
+
// 约定:since 向下取整、until 向上取整,把 since/until 各自所在桶完整纳入。
|
|
151
|
+
//
|
|
152
|
+
// 周界以 ISO 周(周一为周首,UTC);月按 UTC 月初对齐。
|
|
153
|
+
const MS_MIN = 60_000;
|
|
154
|
+
const MS_HOUR = 3_600_000;
|
|
155
|
+
const MS_DAY = 86_400_000;
|
|
156
|
+
/** 把 ms 向下对齐到桶起点(UTC)。未识别的 bucket 原样返回。 */
|
|
157
|
+
function floorMsToBucket(ms, bucket) {
|
|
158
|
+
switch (bucket) {
|
|
159
|
+
case "1m":
|
|
160
|
+
return Math.floor(ms / MS_MIN) * MS_MIN;
|
|
161
|
+
case "1h":
|
|
162
|
+
return Math.floor(ms / MS_HOUR) * MS_HOUR;
|
|
163
|
+
case "1d":
|
|
164
|
+
case "DAY": {
|
|
165
|
+
const d = new Date(ms);
|
|
166
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
167
|
+
}
|
|
168
|
+
case "WEEK": {
|
|
169
|
+
const d = new Date(ms);
|
|
170
|
+
const offsetDays = (d.getUTCDay() + 6) % 7; // 距上一个周一的天数
|
|
171
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offsetDays);
|
|
172
|
+
}
|
|
173
|
+
case "MONTH": {
|
|
174
|
+
const d = new Date(ms);
|
|
175
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return ms;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** 把 ms 向上对齐到下一个桶起点(UTC);恰在边界则不变。 */
|
|
182
|
+
function ceilMsToBucket(ms, bucket) {
|
|
183
|
+
const floor = floorMsToBucket(ms, bucket);
|
|
184
|
+
if (floor === ms)
|
|
185
|
+
return ms;
|
|
186
|
+
switch (bucket) {
|
|
187
|
+
case "1m":
|
|
188
|
+
return floor + MS_MIN;
|
|
189
|
+
case "1h":
|
|
190
|
+
return floor + MS_HOUR;
|
|
191
|
+
case "1d":
|
|
192
|
+
case "DAY":
|
|
193
|
+
return floor + MS_DAY;
|
|
194
|
+
case "WEEK":
|
|
195
|
+
return floor + 7 * MS_DAY;
|
|
196
|
+
case "MONTH": {
|
|
197
|
+
const d = new Date(floor);
|
|
198
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
return ms;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@lark-apaas/http-client": "^0.1.5",
|
|
29
29
|
"commander": "^13.1.0",
|
|
30
|
+
"ora": "^5.4.1",
|
|
30
31
|
"picocolors": "^1.1.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
@@ -53,9 +54,10 @@
|
|
|
53
54
|
"lint": "eslint src/ --max-warnings 0",
|
|
54
55
|
"format": "prettier --write src/",
|
|
55
56
|
"format:check": "prettier --check src/",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"test:watch": "vitest",
|
|
58
|
-
"test:integration": "vitest run --
|
|
59
|
-
"dev": "node --import tsx src/main.ts"
|
|
57
|
+
"test": "vitest run --project=unit",
|
|
58
|
+
"test:watch": "vitest --project=unit",
|
|
59
|
+
"test:integration": "vitest run --project=integration",
|
|
60
|
+
"dev": "node --import tsx src/main.ts",
|
|
61
|
+
"cli": "node --env-file-if-exists=integration/.env --import tsx src/main.ts"
|
|
60
62
|
}
|
|
61
63
|
}
|