@lark-apaas/miaoda-cli 0.1.3 → 0.1.4-alpha.8b6506a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/app/api.js +3 -3
- package/dist/api/app/schemas.js +43 -43
- package/dist/api/db/api.js +398 -55
- package/dist/api/db/client.js +155 -28
- package/dist/api/db/index.js +12 -1
- package/dist/api/db/parsers.js +20 -20
- package/dist/api/db/sql-keywords.js +87 -87
- package/dist/api/deploy/api.js +5 -5
- package/dist/api/deploy/index.js +12 -1
- package/dist/api/deploy/modern-types.js +23 -0
- package/dist/api/deploy/modern.js +70 -0
- package/dist/api/deploy/plugin-instances-types.js +6 -0
- package/dist/api/deploy/plugin-instances.js +30 -0
- package/dist/api/deploy/schemas.js +32 -32
- package/dist/api/file/api.js +89 -87
- package/dist/api/file/client.js +62 -22
- package/dist/api/file/detect.js +3 -3
- package/dist/api/file/index.js +2 -1
- package/dist/api/file/parsers.js +18 -7
- package/dist/api/observability/api.js +6 -6
- package/dist/api/observability/schemas.js +14 -14
- package/dist/api/plugin/api.js +31 -31
- package/dist/cli/commands/app/index.js +94 -13
- package/dist/cli/commands/db/index.js +602 -54
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/deploy/modern.js +48 -0
- package/dist/cli/commands/file/index.js +85 -58
- package/dist/cli/commands/index.js +33 -5
- package/dist/cli/commands/observability/index.js +69 -69
- package/dist/cli/commands/plugin/index.js +27 -27
- package/dist/cli/commands/shared.js +10 -10
- package/dist/cli/commands/skills/index.js +63 -0
- package/dist/cli/handlers/app/index.js +6 -1
- package/dist/cli/handlers/app/init.js +84 -0
- package/dist/cli/handlers/app/update.js +2 -2
- package/dist/cli/handlers/db/_destructive.js +67 -0
- package/dist/cli/handlers/db/_env.js +26 -0
- package/dist/cli/handlers/db/_operator.js +35 -0
- package/dist/cli/handlers/db/audit.js +383 -0
- package/dist/cli/handlers/db/changelog.js +160 -0
- package/dist/cli/handlers/db/data.js +32 -31
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +234 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +413 -0
- package/dist/cli/handlers/db/schema.js +33 -33
- package/dist/cli/handlers/db/sql.js +69 -69
- package/dist/cli/handlers/deploy/deploy.js +4 -4
- package/dist/cli/handlers/deploy/error-log.js +1 -1
- package/dist/cli/handlers/deploy/get.js +3 -3
- package/dist/cli/handlers/deploy/modern.js +32 -0
- package/dist/cli/handlers/deploy/polling.js +11 -11
- package/dist/cli/handlers/file/cp.js +30 -30
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +5 -5
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +32 -30
- package/dist/cli/handlers/file/sign.js +3 -3
- package/dist/cli/handlers/file/stat.js +10 -9
- package/dist/cli/handlers/observability/analytics.js +47 -47
- package/dist/cli/handlers/observability/helpers.js +2 -2
- package/dist/cli/handlers/observability/log.js +9 -9
- package/dist/cli/handlers/observability/metric.js +26 -26
- package/dist/cli/handlers/observability/trace.js +5 -5
- package/dist/cli/handlers/plugin/plugin-local.js +53 -53
- package/dist/cli/handlers/plugin/plugin.js +15 -15
- package/dist/cli/handlers/skills/index.js +7 -0
- package/dist/cli/handlers/skills/status.js +31 -0
- package/dist/cli/handlers/skills/sync.js +38 -0
- package/dist/cli/help.js +16 -16
- package/dist/main.js +12 -12
- package/dist/services/app/init/index.js +12 -0
- package/dist/services/app/init/install.js +101 -0
- package/dist/services/app/init/template.js +85 -0
- package/dist/services/deploy/modern/atoms/build.js +54 -0
- package/dist/services/deploy/modern/atoms/context.js +30 -0
- package/dist/services/deploy/modern/atoms/index.js +17 -0
- package/dist/services/deploy/modern/atoms/local-release.js +27 -0
- package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
- package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
- package/dist/services/deploy/modern/atoms/upload.js +205 -0
- package/dist/services/deploy/modern/check.js +58 -0
- package/dist/services/deploy/modern/constants.js +13 -0
- package/dist/services/deploy/modern/index.js +16 -0
- package/dist/services/deploy/modern/pipelines/index.js +5 -0
- package/dist/services/deploy/modern/pipelines/local.js +75 -0
- package/dist/services/deploy/modern/protocol.js +43 -0
- package/dist/services/deploy/modern/run-types.js +4 -0
- package/dist/services/deploy/modern/run.js +13 -0
- package/dist/services/deploy/modern/template-key-map.js +29 -0
- package/dist/services/skills/index.js +5 -0
- package/dist/services/skills/status.js +37 -0
- package/dist/utils/args.js +1 -1
- package/dist/utils/coding-steering.js +84 -0
- 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 +28 -26
- package/dist/utils/index.js +3 -1
- package/dist/utils/npm-pack.js +55 -0
- package/dist/utils/output.js +67 -45
- package/dist/utils/poll.js +35 -0
- package/dist/utils/render.js +27 -27
- package/dist/utils/spark-meta.js +42 -0
- package/dist/utils/spinner.js +46 -0
- package/dist/utils/time.js +47 -42
- package/package.json +1 -1
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,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readSparkMeta = readSparkMeta;
|
|
7
|
+
exports.writeSparkMeta = writeSparkMeta;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const SPARK_DIR = '.spark';
|
|
11
|
+
const SPARK_META_FILE = 'meta.json';
|
|
12
|
+
function readSparkMeta(appDir) {
|
|
13
|
+
const metaPath = node_path_1.default.join(appDir, SPARK_DIR, SPARK_META_FILE);
|
|
14
|
+
if (!node_fs_1.default.existsSync(metaPath))
|
|
15
|
+
return {};
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* merge 写入:已有字段保留,undefined 不覆盖;不再维护时间戳。
|
|
25
|
+
*/
|
|
26
|
+
function writeSparkMeta(appDir, meta) {
|
|
27
|
+
const sparkDir = node_path_1.default.join(appDir, SPARK_DIR);
|
|
28
|
+
node_fs_1.default.mkdirSync(sparkDir, { recursive: true });
|
|
29
|
+
const metaPath = node_path_1.default.join(sparkDir, SPARK_META_FILE);
|
|
30
|
+
let existing = {};
|
|
31
|
+
if (node_fs_1.default.existsSync(metaPath)) {
|
|
32
|
+
try {
|
|
33
|
+
existing = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* 读不出就当空 */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const defined = Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined));
|
|
40
|
+
const merged = { ...existing, ...defined };
|
|
41
|
+
node_fs_1.default.writeFileSync(metaPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/utils/time.js
CHANGED
|
@@ -10,45 +10,48 @@ exports.parseToSec = parseToSec;
|
|
|
10
10
|
exports.floorMsToBucket = floorMsToBucket;
|
|
11
11
|
exports.ceilMsToBucket = ceilMsToBucket;
|
|
12
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
13
|
/**
|
|
20
|
-
*
|
|
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')、单独年份等。
|
|
14
|
+
* 时间格式简短说明,对齐 Supabase / fly.io 等主流 CLI 的「简洁但严格」风格。
|
|
30
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。
|
|
31
30
|
* 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
|
|
32
31
|
*/
|
|
33
32
|
function parseTimeToMs(input, now = new Date()) {
|
|
34
|
-
// 1. 相对时间:30m / 1h / 2d / 1w
|
|
35
|
-
const relative = /^(\d+)([
|
|
33
|
+
// 1. 相对时间:30s / 30m / 1h / 2d / 1w
|
|
34
|
+
const relative = /^(\d+)([smhdw])$/.exec(input);
|
|
36
35
|
if (relative) {
|
|
37
36
|
const n = Number(relative[1]);
|
|
37
|
+
if (n <= 0)
|
|
38
|
+
failInvalidTimestamp(input);
|
|
38
39
|
const unit = relative[2];
|
|
39
|
-
const factor = unit ===
|
|
40
|
-
?
|
|
41
|
-
: unit ===
|
|
42
|
-
?
|
|
43
|
-
: unit ===
|
|
44
|
-
?
|
|
45
|
-
:
|
|
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;
|
|
46
49
|
return now.getTime() - n * factor;
|
|
47
50
|
}
|
|
48
51
|
// 2. 纯日期:YYYY-MM-DD → 本地当日 00:00:00
|
|
49
52
|
const date = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
|
|
50
53
|
if (date) {
|
|
51
|
-
return localDateMs(input, date[1], date[2], date[3],
|
|
54
|
+
return localDateMs(input, date[1], date[2], date[3], '00', '00', '00', '0');
|
|
52
55
|
}
|
|
53
56
|
// 3. 本地日期+时间:YYYY-MM-DDTHH:mm:ss[.SSS](不带时区,T 分隔)。
|
|
54
57
|
// 不接受空格分隔——'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数,
|
|
@@ -57,14 +60,14 @@ function parseTimeToMs(input, now = new Date()) {
|
|
|
57
60
|
if (localDt) {
|
|
58
61
|
// capture 7 是可选 .SSS 组,运行时可能 undefined;TS lib 的 RegExp match 索引
|
|
59
62
|
// 类型把它标成 string,所以这里显式 cast 让 ?? 在 lint 视角下也"必要"。
|
|
60
|
-
const msPart = localDt[7] ??
|
|
63
|
+
const msPart = localDt[7] ?? '0';
|
|
61
64
|
return localDateMs(input, localDt[1], localDt[2], localDt[3], localDt[4], localDt[5], localDt[6], msPart);
|
|
62
65
|
}
|
|
63
66
|
// 4. 带显式时区的 ISO 8601:YYYY-MM-DDTHH:mm:ss[.SSS](Z|±HH:MM|±HHMM)
|
|
64
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);
|
|
65
68
|
if (iso) {
|
|
66
69
|
validateDateTimeParts(input, iso[1], iso[2], iso[3], iso[4], iso[5], iso[6]);
|
|
67
|
-
if (iso[7] !==
|
|
70
|
+
if (iso[7] !== 'Z')
|
|
68
71
|
validateOffset(input, iso[8], iso[9]);
|
|
69
72
|
const ms = Date.parse(input);
|
|
70
73
|
if (Number.isNaN(ms))
|
|
@@ -76,7 +79,7 @@ function parseTimeToMs(input, now = new Date()) {
|
|
|
76
79
|
function localDateMs(input, y, mo, d, h, mi, s, msPart) {
|
|
77
80
|
validateDateTimeParts(input, y, mo, d, h, mi, s);
|
|
78
81
|
// 毫秒位补齐到 3 位再截断("5" → 500,"12" → 120,"123" → 123)
|
|
79
|
-
const ms = Number(msPart.padEnd(3,
|
|
82
|
+
const ms = Number(msPart.padEnd(3, '0').slice(0, 3));
|
|
80
83
|
const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s), ms);
|
|
81
84
|
if (date.getFullYear() !== Number(y) ||
|
|
82
85
|
date.getMonth() !== Number(mo) - 1 ||
|
|
@@ -115,7 +118,9 @@ function validateOffset(input, h, mi) {
|
|
|
115
118
|
failInvalidTimestamp(input);
|
|
116
119
|
}
|
|
117
120
|
function failInvalidTimestamp(input) {
|
|
118
|
-
throw new error_1.AppError(
|
|
121
|
+
throw new error_1.AppError('ARGS_INVALID', `Invalid timestamp '${input}'`, {
|
|
122
|
+
next_actions: [exports.TIMESTAMP_HELP],
|
|
123
|
+
});
|
|
119
124
|
}
|
|
120
125
|
/** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
|
|
121
126
|
function msToNs(ms) {
|
|
@@ -156,21 +161,21 @@ const MS_DAY = 86_400_000;
|
|
|
156
161
|
/** 把 ms 向下对齐到桶起点(UTC)。未识别的 bucket 原样返回。 */
|
|
157
162
|
function floorMsToBucket(ms, bucket) {
|
|
158
163
|
switch (bucket) {
|
|
159
|
-
case
|
|
164
|
+
case '1m':
|
|
160
165
|
return Math.floor(ms / MS_MIN) * MS_MIN;
|
|
161
|
-
case
|
|
166
|
+
case '1h':
|
|
162
167
|
return Math.floor(ms / MS_HOUR) * MS_HOUR;
|
|
163
|
-
case
|
|
164
|
-
case
|
|
168
|
+
case '1d':
|
|
169
|
+
case 'DAY': {
|
|
165
170
|
const d = new Date(ms);
|
|
166
171
|
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
167
172
|
}
|
|
168
|
-
case
|
|
173
|
+
case 'WEEK': {
|
|
169
174
|
const d = new Date(ms);
|
|
170
175
|
const offsetDays = (d.getUTCDay() + 6) % 7; // 距上一个周一的天数
|
|
171
176
|
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offsetDays);
|
|
172
177
|
}
|
|
173
|
-
case
|
|
178
|
+
case 'MONTH': {
|
|
174
179
|
const d = new Date(ms);
|
|
175
180
|
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
176
181
|
}
|
|
@@ -184,16 +189,16 @@ function ceilMsToBucket(ms, bucket) {
|
|
|
184
189
|
if (floor === ms)
|
|
185
190
|
return ms;
|
|
186
191
|
switch (bucket) {
|
|
187
|
-
case
|
|
192
|
+
case '1m':
|
|
188
193
|
return floor + MS_MIN;
|
|
189
|
-
case
|
|
194
|
+
case '1h':
|
|
190
195
|
return floor + MS_HOUR;
|
|
191
|
-
case
|
|
192
|
-
case
|
|
196
|
+
case '1d':
|
|
197
|
+
case 'DAY':
|
|
193
198
|
return floor + MS_DAY;
|
|
194
|
-
case
|
|
199
|
+
case 'WEEK':
|
|
195
200
|
return floor + 7 * MS_DAY;
|
|
196
|
-
case
|
|
201
|
+
case 'MONTH': {
|
|
197
202
|
const d = new Date(floor);
|
|
198
203
|
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
|
|
199
204
|
}
|