@lark-apaas/miaoda-cli 0.1.2 → 0.1.3-alpha.09899c4
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/db/api.js +390 -55
- package/dist/api/db/client.js +65 -25
- 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 +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/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/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/api/plugin/api.js +31 -31
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +600 -59
- package/dist/cli/commands/deploy/index.js +155 -0
- package/dist/cli/commands/file/index.js +91 -63
- package/dist/cli/commands/index.js +10 -6
- package/dist/cli/commands/observability/index.js +240 -0
- package/dist/cli/commands/plugin/index.js +27 -27
- package/dist/cli/commands/shared.js +86 -10
- 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/_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 +34 -34
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +245 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +387 -0
- package/dist/cli/handlers/db/schema.js +35 -36
- package/dist/cli/handlers/db/sql.js +70 -71
- 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 +31 -32
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +6 -7
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +33 -32
- package/dist/cli/handlers/file/sign.js +4 -5
- package/dist/cli/handlers/file/stat.js +11 -11
- 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/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 +13 -9
- package/dist/utils/args.js +8 -0
- package/dist/utils/colors.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/error.js +2 -2
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +119 -1
- package/dist/utils/index.js +15 -1
- package/dist/utils/output.js +373 -20
- 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 +208 -0
- package/package.json +7 -5
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* 时间格式简短说明,对齐 Supabase / fly.io 等主流 CLI 的「简洁但严格」风格。
|
|
15
|
+
*
|
|
16
|
+
* - 不接受 YYYY/MM/DD、'April 1 2026' 这类松散 / 区域性形式(Date.parse 会误吞)
|
|
17
|
+
* - 不接受空格分隔的 'YYYY-MM-DD HH:mm:ss':不带引号会被 shell 拆成两个参数,
|
|
18
|
+
* 宁可拒绝也不让用户踩「看起来传了 since 实际只传了一半」的坑
|
|
19
|
+
* - 不带显式时区的 (YYYY-MM-DD / YYYY-MM-DDTHH:mm:ss) 一律按**本地时区**解释,
|
|
20
|
+
* 跟 pretty 输出(renderDate 用 getFullYear 等本地方法)形成输入/输出闭环
|
|
21
|
+
* ——用户复制 pretty 输出的时间字符串当 --since 不会差时区
|
|
22
|
+
*/
|
|
23
|
+
exports.TIMESTAMP_HELP = 'Accepted formats: ' +
|
|
24
|
+
'relative (30s / 5m / 2h / 3d / 1w); ' +
|
|
25
|
+
'date YYYY-MM-DD (local 00:00:00); ' +
|
|
26
|
+
'local datetime YYYY-MM-DDTHH:mm:ss (local timezone, T-separated); ' +
|
|
27
|
+
'ISO 8601 with timezone YYYY-MM-DDTHH:mm:ssZ or YYYY-MM-DDTHH:mm:ss+08:00.';
|
|
28
|
+
/**
|
|
29
|
+
* 解析时间字符串到毫秒时间戳;不匹配任一支持格式抛 ARGS_INVALID。
|
|
30
|
+
* 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
|
|
31
|
+
*/
|
|
32
|
+
function parseTimeToMs(input, now = new Date()) {
|
|
33
|
+
// 1. 相对时间:30s / 30m / 1h / 2d / 1w
|
|
34
|
+
const relative = /^(\d+)([smhdw])$/.exec(input);
|
|
35
|
+
if (relative) {
|
|
36
|
+
const n = Number(relative[1]);
|
|
37
|
+
if (n <= 0)
|
|
38
|
+
failInvalidTimestamp(input);
|
|
39
|
+
const unit = relative[2];
|
|
40
|
+
const factor = unit === 's'
|
|
41
|
+
? 1_000
|
|
42
|
+
: unit === 'm'
|
|
43
|
+
? 60_000
|
|
44
|
+
: unit === 'h'
|
|
45
|
+
? 3_600_000
|
|
46
|
+
: unit === 'd'
|
|
47
|
+
? 86_400_000
|
|
48
|
+
: /* w */ 604_800_000;
|
|
49
|
+
return now.getTime() - n * factor;
|
|
50
|
+
}
|
|
51
|
+
// 2. 纯日期:YYYY-MM-DD → 本地当日 00:00:00
|
|
52
|
+
const date = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
|
|
53
|
+
if (date) {
|
|
54
|
+
return localDateMs(input, date[1], date[2], date[3], '00', '00', '00', '0');
|
|
55
|
+
}
|
|
56
|
+
// 3. 本地日期+时间:YYYY-MM-DDTHH:mm:ss[.SSS](不带时区,T 分隔)。
|
|
57
|
+
// 不接受空格分隔——'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数,
|
|
58
|
+
// 宁可不支持也不要让 agent 踩"看起来传了 since 实际只传了一半"的坑。
|
|
59
|
+
const localDt = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(input);
|
|
60
|
+
if (localDt) {
|
|
61
|
+
// capture 7 是可选 .SSS 组,运行时可能 undefined;TS lib 的 RegExp match 索引
|
|
62
|
+
// 类型把它标成 string,所以这里显式 cast 让 ?? 在 lint 视角下也"必要"。
|
|
63
|
+
const msPart = localDt[7] ?? '0';
|
|
64
|
+
return localDateMs(input, localDt[1], localDt[2], localDt[3], localDt[4], localDt[5], localDt[6], msPart);
|
|
65
|
+
}
|
|
66
|
+
// 4. 带显式时区的 ISO 8601:YYYY-MM-DDTHH:mm:ss[.SSS](Z|±HH:MM|±HHMM)
|
|
67
|
+
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);
|
|
68
|
+
if (iso) {
|
|
69
|
+
validateDateTimeParts(input, iso[1], iso[2], iso[3], iso[4], iso[5], iso[6]);
|
|
70
|
+
if (iso[7] !== 'Z')
|
|
71
|
+
validateOffset(input, iso[8], iso[9]);
|
|
72
|
+
const ms = Date.parse(input);
|
|
73
|
+
if (Number.isNaN(ms))
|
|
74
|
+
failInvalidTimestamp(input);
|
|
75
|
+
return ms;
|
|
76
|
+
}
|
|
77
|
+
failInvalidTimestamp(input);
|
|
78
|
+
}
|
|
79
|
+
function localDateMs(input, y, mo, d, h, mi, s, msPart) {
|
|
80
|
+
validateDateTimeParts(input, y, mo, d, h, mi, s);
|
|
81
|
+
// 毫秒位补齐到 3 位再截断("5" → 500,"12" → 120,"123" → 123)
|
|
82
|
+
const ms = Number(msPart.padEnd(3, '0').slice(0, 3));
|
|
83
|
+
const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s), ms);
|
|
84
|
+
if (date.getFullYear() !== Number(y) ||
|
|
85
|
+
date.getMonth() !== Number(mo) - 1 ||
|
|
86
|
+
date.getDate() !== Number(d) ||
|
|
87
|
+
date.getHours() !== Number(h) ||
|
|
88
|
+
date.getMinutes() !== Number(mi) ||
|
|
89
|
+
date.getSeconds() !== Number(s) ||
|
|
90
|
+
date.getMilliseconds() !== ms) {
|
|
91
|
+
failInvalidTimestamp(input);
|
|
92
|
+
}
|
|
93
|
+
return date.getTime();
|
|
94
|
+
}
|
|
95
|
+
function validateDateTimeParts(input, y, mo, d, h, mi, s) {
|
|
96
|
+
const year = Number(y);
|
|
97
|
+
const month = Number(mo);
|
|
98
|
+
const day = Number(d);
|
|
99
|
+
const hour = Number(h);
|
|
100
|
+
const minute = Number(mi);
|
|
101
|
+
const second = Number(s);
|
|
102
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) {
|
|
103
|
+
failInvalidTimestamp(input);
|
|
104
|
+
}
|
|
105
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
106
|
+
if (utc.getUTCFullYear() !== year ||
|
|
107
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
108
|
+
utc.getUTCDate() !== day) {
|
|
109
|
+
failInvalidTimestamp(input);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function validateOffset(input, h, mi) {
|
|
113
|
+
if (h === undefined || mi === undefined)
|
|
114
|
+
failInvalidTimestamp(input);
|
|
115
|
+
const hour = Number(h);
|
|
116
|
+
const minute = Number(mi);
|
|
117
|
+
if (hour > 23 || minute > 59)
|
|
118
|
+
failInvalidTimestamp(input);
|
|
119
|
+
}
|
|
120
|
+
function failInvalidTimestamp(input) {
|
|
121
|
+
throw new error_1.AppError('ARGS_INVALID', `Invalid timestamp '${input}'`, {
|
|
122
|
+
next_actions: [exports.TIMESTAMP_HELP],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
|
|
126
|
+
function msToNs(ms) {
|
|
127
|
+
return `${String(ms)}000000`;
|
|
128
|
+
}
|
|
129
|
+
/** 毫秒 → 秒(字符串,向下取整) */
|
|
130
|
+
function msToSec(ms) {
|
|
131
|
+
return String(Math.floor(ms / 1000));
|
|
132
|
+
}
|
|
133
|
+
/** 解析到毫秒数;input 为空返回 undefined */
|
|
134
|
+
function parseToMs(input, now) {
|
|
135
|
+
if (!input)
|
|
136
|
+
return undefined;
|
|
137
|
+
return parseTimeToMs(input, now);
|
|
138
|
+
}
|
|
139
|
+
/** 解析到纳秒字符串;input 为空返回 undefined */
|
|
140
|
+
function parseToNs(input, now) {
|
|
141
|
+
if (!input)
|
|
142
|
+
return undefined;
|
|
143
|
+
return msToNs(parseTimeToMs(input, now));
|
|
144
|
+
}
|
|
145
|
+
/** 解析到秒字符串;input 为空返回 undefined */
|
|
146
|
+
function parseToSec(input, now) {
|
|
147
|
+
if (!input)
|
|
148
|
+
return undefined;
|
|
149
|
+
return msToSec(parseTimeToMs(input, now));
|
|
150
|
+
}
|
|
151
|
+
// ── 桶边界对齐 ──
|
|
152
|
+
//
|
|
153
|
+
// metric 的 down-sample 与 analytics 的 timeAggregationUnit 都按 UTC 桶切片。
|
|
154
|
+
// 当用户传的 since/until 落在桶中间,服务端可能返回不到该桶(边界数据丢失)。
|
|
155
|
+
// 约定:since 向下取整、until 向上取整,把 since/until 各自所在桶完整纳入。
|
|
156
|
+
//
|
|
157
|
+
// 周界以 ISO 周(周一为周首,UTC);月按 UTC 月初对齐。
|
|
158
|
+
const MS_MIN = 60_000;
|
|
159
|
+
const MS_HOUR = 3_600_000;
|
|
160
|
+
const MS_DAY = 86_400_000;
|
|
161
|
+
/** 把 ms 向下对齐到桶起点(UTC)。未识别的 bucket 原样返回。 */
|
|
162
|
+
function floorMsToBucket(ms, bucket) {
|
|
163
|
+
switch (bucket) {
|
|
164
|
+
case '1m':
|
|
165
|
+
return Math.floor(ms / MS_MIN) * MS_MIN;
|
|
166
|
+
case '1h':
|
|
167
|
+
return Math.floor(ms / MS_HOUR) * MS_HOUR;
|
|
168
|
+
case '1d':
|
|
169
|
+
case 'DAY': {
|
|
170
|
+
const d = new Date(ms);
|
|
171
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
172
|
+
}
|
|
173
|
+
case 'WEEK': {
|
|
174
|
+
const d = new Date(ms);
|
|
175
|
+
const offsetDays = (d.getUTCDay() + 6) % 7; // 距上一个周一的天数
|
|
176
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offsetDays);
|
|
177
|
+
}
|
|
178
|
+
case 'MONTH': {
|
|
179
|
+
const d = new Date(ms);
|
|
180
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
return ms;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** 把 ms 向上对齐到下一个桶起点(UTC);恰在边界则不变。 */
|
|
187
|
+
function ceilMsToBucket(ms, bucket) {
|
|
188
|
+
const floor = floorMsToBucket(ms, bucket);
|
|
189
|
+
if (floor === ms)
|
|
190
|
+
return ms;
|
|
191
|
+
switch (bucket) {
|
|
192
|
+
case '1m':
|
|
193
|
+
return floor + MS_MIN;
|
|
194
|
+
case '1h':
|
|
195
|
+
return floor + MS_HOUR;
|
|
196
|
+
case '1d':
|
|
197
|
+
case 'DAY':
|
|
198
|
+
return floor + MS_DAY;
|
|
199
|
+
case 'WEEK':
|
|
200
|
+
return floor + 7 * MS_DAY;
|
|
201
|
+
case 'MONTH': {
|
|
202
|
+
const d = new Date(floor);
|
|
203
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
|
|
204
|
+
}
|
|
205
|
+
default:
|
|
206
|
+
return ms;
|
|
207
|
+
}
|
|
208
|
+
}
|
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-alpha.09899c4",
|
|
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
|
}
|