@lark-apaas/miaoda-cli 0.1.2-alpha.c7d0c89 → 0.1.2-alpha.ccfdc05

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 CHANGED
@@ -14,22 +14,22 @@ pnpm add -g @lark-apaas/miaoda-cli
14
14
  # 设置默认应用(需要应用上下文的命令可用 --app-id 覆盖)
15
15
  export MIAODA_APP_ID=app_demo_xxx
16
16
 
17
- # 插件管理
18
- miaoda plugin list-packages
19
- miaoda plugin install @demo/example-plugin
17
+ # 文件操作
18
+ miaoda file ls
19
+ miaoda file upload ./local/path
20
20
  ```
21
21
 
22
22
  JSON 结构化输出(Agent 推荐):
23
23
 
24
24
  ```bash
25
25
  # 输出全部字段
26
- miaoda plugin list --json
26
+ miaoda file ls --json
27
27
 
28
28
  # 可选字段级选择
29
- miaoda plugin list --json id,name
29
+ miaoda file ls --json id,name
30
30
 
31
31
  # 或通过 --output 指定格式
32
- miaoda plugin list --output json
32
+ miaoda file ls --output json
33
33
  ```
34
34
 
35
35
  ## 命令树
@@ -38,7 +38,8 @@ miaoda plugin list --output json
38
38
 
39
39
  | 域 | 用途 |
40
40
  |---|---|
41
- | `miaoda plugin ...` | 插件管理:安装、更新、移除、插件实例查询 |
41
+ | `miaoda file ...` | 文件操作:上传、下载、元数据、签名下载、批量删除 |
42
+ | `miaoda db ...` | 数据操作:SQL 执行、表结构查询、数据导入导出 |
42
43
 
43
44
  完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
44
45
 
@@ -97,7 +97,7 @@ exports.errorJobSchema = {
97
97
  columns: [
98
98
  { key: "jobID", label: "job-id" },
99
99
  { key: "componentName", label: "component" },
100
- { key: "errorMsg", label: "error", format: output_1.fmt.truncate(120) },
100
+ { key: "errorMsg", label: "error" },
101
101
  ],
102
102
  strict: true,
103
103
  };
@@ -6,7 +6,28 @@ const output_1 = require("../../utils/output");
6
6
  exports.logItemSchema = {
7
7
  columns: [
8
8
  { key: "timestampNs", label: "time", format: output_1.fmt.ns("yyyy-MM-dd HH:mm:ss.SSS") },
9
- { key: "severityText", label: "level" },
9
+ {
10
+ key: "module",
11
+ derive: (row) => {
12
+ return row.attributes?.module;
13
+ },
14
+ },
15
+ {
16
+ key: "user-id",
17
+ derive: (row) => {
18
+ return row.attributes?.user_id;
19
+ },
20
+ },
21
+ { key: "severityText", label: "severity-text" },
22
+ {
23
+ key: "duration",
24
+ format: output_1.fmt.durationMs(),
25
+ derive: (row) => {
26
+ return row.attributes?.duration_ms;
27
+ },
28
+ },
29
+ { key: "traceID", label: "trace-id" },
30
+ { key: "id", label: "log-id" },
10
31
  { key: "body" },
11
32
  ],
12
33
  strict: true,
@@ -6,9 +6,13 @@ const shared_1 = require("../../../cli/commands/shared");
6
6
  const index_1 = require("../../../cli/handlers/deploy/index");
7
7
  const COMMON_TIME_HELP = `
8
8
  时间格式:
9
- - 相对时间:1h1 小时前)、2d(2 天前)、1w(1 周前)、30m(30 分钟前)
10
- - 日期:2026-04-01(按当日 00:00:00 UTC
11
- - ISO 8601:2026-04-01T10:00:00Z
9
+ - 相对时间:30m30 分钟前)、1h、2d1w
10
+ - 日期:2026-04-01(本地时区当日 00:00:00)
11
+ - 本地日期+时间:2026-04-01T10:00:00(按本地时区,T 分隔)
12
+ - 带时区 ISO:2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)
13
+ 备注:
14
+ 1) 不带时区的形式与 pretty 输出闭环(同机器复制粘贴稳定);跨机器请带显式时区。
15
+ 2) 必须用 T 分隔日期与时间,禁止空格——shell 会把不带引号的 'YYYY-MM-DD HH:mm:ss' 拆成两个参数。
12
16
  `;
13
17
  function registerDeployCommands(program) {
14
18
  // PRD:`miaoda deploy` 自身即为触发发布;get / history / error-log 是子命令
@@ -20,6 +24,12 @@ function registerDeployCommands(program) {
20
24
  .option("--wait", "阻塞直到流水线终态", false)
21
25
  .option("--timeout <sec>", "--wait 最长等待秒数(默认 300)", parseTimeout, 300)
22
26
  .addHelpText("after", `
27
+ 部署前置检查(Agent 必须执行)
28
+ miaoda deploy 跑的是当前分支的远端 HEAD——本地未 commit / 未 push 的代码不会进发布产物。
29
+ 调用前先 \`git status\` + \`git rev-list --count @{u}..HEAD\` 确认;有未提交 / 未推送时:
30
+ - 是本次任务的产物 → 先 commit + push 再 deploy
31
+ - 不能确定意图 → 用 AskUser 跟用户确认(不要默默继续)
32
+
23
33
  JSON 输出
24
34
  不带 --wait:{"data": {"pipelineTaskID": "..."}}(pipelineTaskID 即 deploy-id)
25
35
  带 --wait:data 额外包含 detail(pipeline 终态)
@@ -70,14 +80,9 @@ function registerDeployHistory(parent) {
70
80
  .command("history")
71
81
  .description("查询发布历史(按时间倒序,分页)")
72
82
  .addOption((0, shared_1.appIdOption)().hideHelp())
73
- .addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline 节点状态)").choices([
74
- "todo",
75
- "running",
76
- "success",
77
- "failed",
78
- "canceled",
79
- "hold_on",
80
- ]))
83
+ .addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline 节点状态,大小写不敏感)")
84
+ .choices(["todo", "running", "success", "failed", "canceled", "hold_on"])
85
+ .argParser((0, shared_1.caseInsensitiveChoice)(["todo", "running", "success", "failed", "canceled", "hold_on"])))
81
86
  .option("--since <time>", "起始时间")
82
87
  .option("--until <time>", "截止时间")
83
88
  .option("--limit <n>", "返回条数上限(1~100)", parseLimit, 50)
@@ -91,6 +96,7 @@ JSON 输出
91
96
  $ miaoda deploy history --since 7d
92
97
  `);
93
98
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
99
+ (0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
94
100
  (0, shared_1.rejectCliOverride)(cmd, "appId");
95
101
  await (0, index_1.handleDeployHistory)({
96
102
  appId: rawOpts.appId,
@@ -1,17 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCommands = registerCommands;
4
- const index_1 = require("../../cli/commands/plugin/index");
5
- const index_2 = require("../../cli/commands/file/index");
6
- const index_3 = require("../../cli/commands/db/index");
7
- const index_4 = require("../../cli/commands/observability/index");
8
- const index_5 = require("../../cli/commands/app/index");
9
- const index_6 = require("../../cli/commands/deploy/index");
4
+ const index_1 = require("../../cli/commands/file/index");
5
+ const index_2 = require("../../cli/commands/db/index");
6
+ const index_3 = require("../../cli/commands/observability/index");
7
+ const index_4 = require("../../cli/commands/app/index");
8
+ const index_5 = require("../../cli/commands/deploy/index");
10
9
  function registerCommands(program) {
11
- (0, index_1.registerPluginCommands)(program);
12
- (0, index_2.registerFileCommands)(program);
13
- (0, index_3.registerDbCommands)(program);
14
- (0, index_4.registerObservabilityCommands)(program);
15
- (0, index_5.registerAppCommands)(program);
16
- (0, index_6.registerDeployCommands)(program);
10
+ (0, index_1.registerFileCommands)(program);
11
+ (0, index_2.registerDbCommands)(program);
12
+ (0, index_3.registerObservabilityCommands)(program);
13
+ (0, index_4.registerAppCommands)(program);
14
+ (0, index_5.registerDeployCommands)(program);
17
15
  }
@@ -6,9 +6,13 @@ const shared_1 = require("../../../cli/commands/shared");
6
6
  const index_1 = require("../../../cli/handlers/observability/index");
7
7
  const COMMON_TIME_HELP = `
8
8
  时间格式:
9
- - 相对时间:1h1 小时前)、2d(2 天前)、1w(1 周前)、30m(30 分钟前)
10
- - 日期:2026-04-01(按当日 00:00:00 UTC
11
- - ISO 8601:2026-04-01T10:00:00Z
9
+ - 相对时间:30m30 分钟前)、1h、2d1w
10
+ - 日期:2026-04-01(本地时区当日 00:00:00)
11
+ - 本地日期+时间:2026-04-01T10:00:00(按本地时区,T 分隔)
12
+ - 带时区 ISO:2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)
13
+ 备注:
14
+ 1) 不带时区的形式与 pretty 输出闭环(同机器复制粘贴稳定);跨机器请带显式时区。
15
+ 2) 必须用 T 分隔日期与时间,禁止空格——shell 会把不带引号的 'YYYY-MM-DD HH:mm:ss' 拆成两个参数。
12
16
  `;
13
17
  function registerObservabilityCommands(program) {
14
18
  const obCmd = program
@@ -23,8 +27,7 @@ function registerObservabilityCommands(program) {
23
27
  应用上下文:--app-id <id> 或环境变量 MIAODA_APP_ID。
24
28
 
25
29
  应用环境
26
- log/trace 命令需要 --env <dev|online>,默认 online,可设置 MIAODA_APP_ENV 覆盖。
27
- metric/analytics 不需要 --env。
30
+ 目前只支持线上环境。
28
31
  `);
29
32
  registerLog(obCmd);
30
33
  registerTrace(obCmd);
@@ -37,7 +40,9 @@ function registerLog(parent) {
37
40
  .command("log")
38
41
  .description("查询线上运行日志(按时间倒序,分页)")
39
42
  .addOption((0, shared_1.appIdOption)().hideHelp())
40
- .addOption(new commander_1.Option("--level <level>", "日志级别").choices(["DEBUG", "INFO", "WARN", "ERROR"]))
43
+ .addOption(new commander_1.Option("--level <level>", "日志级别(大小写不敏感)")
44
+ .choices(["DEBUG", "INFO", "WARN", "ERROR"])
45
+ .argParser((0, shared_1.caseInsensitiveChoice)(["DEBUG", "INFO", "WARN", "ERROR"])))
41
46
  .option("--since <time>", "开始时间")
42
47
  .option("--until <time>", "截止时间")
43
48
  .option("--trace-id <id>", "按 trace ID 过滤")
@@ -60,6 +65,7 @@ JSON 输出
60
65
  $ miaoda observability log --api /api/orders --min-duration 200 --json
61
66
  `);
62
67
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
68
+ (0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
63
69
  (0, shared_1.rejectCliOverride)(cmd, "appId");
64
70
  await (0, index_1.handleObservabilityLog)({
65
71
  appId: rawOpts.appId,
@@ -105,6 +111,7 @@ JSON 输出
105
111
  $ miaoda observability trace list --root-span api-gateway --limit 20 --json
106
112
  `);
107
113
  listCmd.action((0, shared_1.withHelp)(listCmd, async (rawOpts) => {
114
+ (0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
108
115
  (0, shared_1.rejectCliOverride)(listCmd, "appId");
109
116
  await (0, index_1.handleObservabilityTraceList)({
110
117
  appId: rawOpts.appId,
@@ -156,9 +163,10 @@ function registerMetric(parent) {
156
163
  .option("--series <name>", "过滤图表中的某条线:latency 取 p50/p99;requests 取 total/error;缺省返回所有相关线")
157
164
  .option("--since <time>", "开始时间")
158
165
  .option("--until <time>", "截止时间")
159
- .addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1天)")
166
+ .addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1天,大小写不敏感)")
160
167
  .choices(["1m", "1h", "1d"])
161
- .default("1h"))
168
+ .argParser((0, shared_1.caseInsensitiveChoice)(["1m", "1h", "1d"]))
169
+ .default("1m"))
162
170
  .addHelpText("after", `${COMMON_TIME_HELP}
163
171
  JSON 输出
164
172
  {"data": [{"metricName": "...", "dimensions": {...}, "dataPoints": [...]}], "next_cursor": null, "has_more": false}
@@ -170,6 +178,7 @@ JSON 输出
170
178
  $ miaoda observability metric cpu --since 1d --down-sample 1d --json
171
179
  `);
172
180
  cmd.action((0, shared_1.withHelp)(cmd, async (metricName, rawOpts) => {
181
+ (0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
173
182
  (0, shared_1.rejectCliOverride)(cmd, "appId");
174
183
  await (0, index_1.handleObservabilityMetric)({
175
184
  metricName,
@@ -191,7 +200,7 @@ function registerAnalytics(parent) {
191
200
  .argument("<analytics-name>", "指标名:users | page-view")
192
201
  .addOption((0, shared_1.appIdOption)().hideHelp())
193
202
  .option("--page <path>", "按页面路径过滤(仅对 page-view 生效)")
194
- .option("--series <name>", "过滤图表中的某条线:users 取 active-users/new-users/total-userspage-view 取 all-view/desktop-view/mobile-view;缺省返回所有相关线")
203
+ .option("--series <name>", "过滤图表中的某条线:users 取 active/new/total(兼容 active-users/new-users/total-users);page-view 取 all/desktop/mobile(兼容 all-view/desktop-view/mobile-view")
195
204
  .option("--since <time>", "开始时间")
196
205
  .option("--until <time>", "截止时间")
197
206
  .option("--granularity <duration>", "时间粒度:day | week | month", "day")
@@ -201,10 +210,11 @@ JSON 输出
201
210
 
202
211
  示例
203
212
  $ miaoda observability analytics users --json # 同时返回 active/new/total 三条线
204
- $ miaoda observability analytics users --series new-users --since 7d --json
213
+ $ miaoda observability analytics users --series new --since 7d --json
205
214
  $ miaoda observability analytics page-view --granularity week --json
206
215
  `);
207
216
  cmd.action((0, shared_1.withHelp)(cmd, async (analyticsName, rawOpts) => {
217
+ (0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
208
218
  (0, shared_1.rejectCliOverride)(cmd, "appId");
209
219
  await (0, index_1.handleObservabilityAnalytics)({
210
220
  analyticsName,
@@ -6,9 +6,12 @@ exports.softRequiredOption = softRequiredOption;
6
6
  exports.resolveAppId = resolveAppId;
7
7
  exports.withHelp = withHelp;
8
8
  exports.failArgs = failArgs;
9
+ exports.caseInsensitiveChoice = caseInsensitiveChoice;
10
+ exports.validateTimeOptions = validateTimeOptions;
9
11
  exports.rejectCliOverride = rejectCliOverride;
10
12
  const commander_1 = require("commander");
11
13
  const error_1 = require("../../utils/error");
14
+ const time_1 = require("../../utils/time");
12
15
  /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption()) */
13
16
  function appIdOption() {
14
17
  return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
@@ -62,6 +65,47 @@ function withHelp(cmd, handler) {
62
65
  function failArgs(message) {
63
66
  throw new error_1.AppError("ARGS_INVALID", message);
64
67
  }
68
+ /**
69
+ * 大小写不敏感的 choice argParser:把用户输入按 lowerCase 匹配回 canonical 数组里
70
+ * 的同名值(保留 canonical 大小写),未命中抛 Commander 的 InvalidArgumentError
71
+ * (exit code 1,自带友好错误)。
72
+ *
73
+ * 必须先 .choices(canonical)、再 .argParser()——Commander 内部 .choices() 会
74
+ * 重置 parseArg,反过来调会让 argParser 失效。.choices() 仅留给 help 渲染
75
+ * "choices: A, B, C",实际白名单校验由 argParser 接管。
76
+ *
77
+ * new Option("--level <level>", "日志级别(不区分大小写)")
78
+ * .choices(["DEBUG", "INFO", "WARN", "ERROR"])
79
+ * .argParser(caseInsensitiveChoice(["DEBUG", "INFO", "WARN", "ERROR"]));
80
+ */
81
+ function caseInsensitiveChoice(canonical) {
82
+ const map = new Map(canonical.map((v) => [v.toLowerCase(), v]));
83
+ return (raw) => {
84
+ const hit = map.get(raw.toLowerCase());
85
+ if (hit === undefined) {
86
+ throw new commander_1.InvalidArgumentError(`Allowed choices are ${canonical.join(", ")} (case-insensitive).`);
87
+ }
88
+ return hit;
89
+ };
90
+ }
91
+ /**
92
+ * --since / --until 等时间参数的 action 阶段校验。
93
+ *
94
+ * Commander 的 argParser 会在 action 前直接按 parser error 退出(exit 1),
95
+ * 绕过 withHelp 的 ARGS_INVALID → exit 2 路径;所以时间参数在 action 内先
96
+ * 校验一遍,再把原始字符串交给 handler 解析成接口需要的单位。
97
+ */
98
+ function validateTimeOptions(opts, ...names) {
99
+ for (const name of names) {
100
+ const value = opts[name];
101
+ if (value === undefined)
102
+ continue;
103
+ if (typeof value !== "string") {
104
+ failArgs(`--${name.replace(/([A-Z])/g, "-$1").toLowerCase()} 必须是时间字符串`);
105
+ }
106
+ (0, time_1.parseTimeToMs)(value);
107
+ }
108
+ }
65
109
  /**
66
110
  * 拒绝 CLI 显式覆盖:用于 hideHelp() + .env() 的"沙箱注入参数"(如 --app-id / --branch)。
67
111
  *
@@ -149,22 +149,23 @@ function buildAnalyticsPivotSchema(seriesLabels) {
149
149
  */
150
150
  function resolveAnalyticsSelection(cliName, series) {
151
151
  const extras = [];
152
+ const normalizedSeries = normalizeAnalyticsSeries(cliName, series);
152
153
  if (cliName === "users") {
153
- if (series === "active-users")
154
+ if (normalizedSeries === "active-users")
154
155
  return single("ACTIVE_USER", "active-users", extras);
155
- if (series === "new-users")
156
+ if (normalizedSeries === "new-users")
156
157
  return single("NEW_USER", "new-users", extras);
157
- if (series === "total-users")
158
+ if (normalizedSeries === "total-users")
158
159
  return single("TOTAL_USER", "total-users", extras);
159
160
  // 缺省:三条线
160
161
  return all(ANALYTICS_LABELS.users, extras);
161
162
  }
162
163
  if (cliName === "page-view") {
163
- if (series === "desktop-view") {
164
+ if (normalizedSeries === "desktop-view") {
164
165
  extras.push({ key: "device_type", value: (0, helpers_1.eqFilter)("desktop") });
165
166
  return single("PAGE_VIEW", "desktop-view", extras);
166
167
  }
167
- if (series === "mobile-view") {
168
+ if (normalizedSeries === "mobile-view") {
168
169
  extras.push({ key: "device_type", value: (0, helpers_1.eqFilter)("mobile") });
169
170
  return single("PAGE_VIEW", "mobile-view", extras);
170
171
  }
@@ -174,6 +175,27 @@ function resolveAnalyticsSelection(cliName, series) {
174
175
  // 兜底:CLI 名直接当 metricType + label
175
176
  return single(cliName, cliName, extras);
176
177
  }
178
+ function normalizeAnalyticsSeries(cliName, series) {
179
+ if (series === undefined)
180
+ return undefined;
181
+ if (cliName === "users") {
182
+ if (series === "active")
183
+ return "active-users";
184
+ if (series === "new")
185
+ return "new-users";
186
+ if (series === "total")
187
+ return "total-users";
188
+ }
189
+ if (cliName === "page-view") {
190
+ if (series === "all")
191
+ return "all-view";
192
+ if (series === "desktop")
193
+ return "desktop-view";
194
+ if (series === "mobile")
195
+ return "mobile-view";
196
+ }
197
+ return series;
198
+ }
177
199
  function single(metricType, label, extras) {
178
200
  return {
179
201
  metricTypes: [metricType],
@@ -71,7 +71,7 @@ async function handleObservabilityMetric(opts) {
71
71
  ]);
72
72
  // since/until 直接以秒透传到 BAM;当前未做桶对齐(如需开启把 until
73
73
  // 用 ceilMsToBucket(untilMs, downSample as GranularityBucket) 包一层即可)。
74
- const downSample = opts.downSample ?? "1h";
74
+ const downSample = opts.downSample ?? "1m";
75
75
  const nowMs = Date.now();
76
76
  const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
77
77
  const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRootVersionArg = resolveRootVersionArg;
4
+ function resolveRootVersionArg(args) {
5
+ if (args.length === 1 && (args[0] === "-v" || args[0] === "--version"))
6
+ return "show";
7
+ if (args.some(isLooseVersionArg))
8
+ return "invalid-variant";
9
+ if (args.includes("-v") || args.includes("--version"))
10
+ return "invalid-placement";
11
+ return "none";
12
+ }
13
+ function isLooseVersionArg(arg) {
14
+ return ((arg.startsWith("-v") && arg !== "-v") || (arg.startsWith("--version") && arg !== "--version"));
15
+ }
package/dist/main.js CHANGED
@@ -5,10 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const commander_1 = require("commander");
7
7
  const index_1 = require("./cli/commands/index");
8
+ const shared_1 = require("./cli/commands/shared");
8
9
  const help_1 = require("./cli/help");
9
10
  const config_1 = require("./utils/config");
10
11
  const log_id_1 = require("./utils/log_id");
11
12
  const output_1 = require("./utils/output");
13
+ const error_1 = require("./utils/error");
14
+ const version_1 = require("./cli/version");
12
15
  const package_json_1 = __importDefault(require("../package.json"));
13
16
  // MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
14
17
  // 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
@@ -20,11 +23,14 @@ const program = new commander_1.Command();
20
23
  const { version } = package_json_1.default;
21
24
  program
22
25
  .name("miaoda")
23
- .description("妙搭平台 CLI,提供数据服务、文件存储、插件管理等命令行操作。")
26
+ .description("妙搭平台 CLI,提供数据服务、文件存储等命令行操作。")
24
27
  .usage("<command> [flags]")
25
- .version(version, "-v, --version", "显示版本号")
28
+ .option("-v, --version", "显示版本号")
26
29
  .option("--json [fields]", "JSON 输出,可选字段级选择")
27
- .option("--output <format>", "输出格式(pretty|json", "pretty")
30
+ .addOption(new commander_1.Option("--output <format>", "输出格式(pretty | json,大小写不敏感)")
31
+ .choices(["pretty", "json"])
32
+ .argParser((0, shared_1.caseInsensitiveChoice)(["pretty", "json"]))
33
+ .default("pretty"))
28
34
  .option("--verbose", "debug 日志到 stderr")
29
35
  .helpOption("-h, --help", "显示帮助信息")
30
36
  .hook("preAction", (_thisCmd, actionCmd) => {
@@ -33,7 +39,21 @@ program
33
39
  (0, config_1.initConfigFromOpts)(opts);
34
40
  });
35
41
  (0, index_1.registerCommands)(program);
36
- program.parseAsync(process.argv).catch((err) => {
37
- (0, output_1.emitError)(err);
38
- process.exitCode = 1;
39
- });
42
+ const versionArg = (0, version_1.resolveRootVersionArg)(process.argv.slice(2));
43
+ if (versionArg === "show") {
44
+ process.stdout.write(`${version}\n`);
45
+ }
46
+ else if (versionArg === "invalid-variant") {
47
+ (0, output_1.emitError)(new error_1.AppError("ARGS_INVALID", "版本参数仅支持精确的 -v 或 --version"));
48
+ process.exitCode = 2;
49
+ }
50
+ else if (versionArg === "invalid-placement") {
51
+ (0, output_1.emitError)(new error_1.AppError("ARGS_INVALID", "-v / --version 仅可在根命令单独使用,不能与其他命令或参数混用"));
52
+ process.exitCode = 2;
53
+ }
54
+ else {
55
+ program.parseAsync(process.argv).catch((err) => {
56
+ (0, output_1.emitError)(err);
57
+ process.exitCode = 1;
58
+ });
59
+ }
@@ -10,6 +10,8 @@ exports.postInnerApi = postInnerApi;
10
10
  exports.getInnerApi = getInnerApi;
11
11
  const http_client_1 = require("@lark-apaas/http-client");
12
12
  const error_1 = require("./error");
13
+ const config_1 = require("./config");
14
+ const logger_1 = require("./logger");
13
15
  let adminClient;
14
16
  let runtimeClient;
15
17
  /**
@@ -70,11 +72,31 @@ function applyCanaryHeader(reqConfig) {
70
72
  }
71
73
  /** 走管理端 innerapi 的 POST + 信封解析 */
72
74
  async function postInnerApi(url, body, opts) {
73
- return handleInnerEnvelope(await getHttpClient().post(url, body), url, opts);
75
+ const startMs = logRequestStart("POST", url, body);
76
+ let response;
77
+ try {
78
+ response = await getHttpClient().post(url, body);
79
+ }
80
+ catch (err) {
81
+ logResponseFailure("POST", url, err, startMs);
82
+ throw err;
83
+ }
84
+ logResponseEnd("POST", url, response, startMs);
85
+ return handleInnerEnvelope(response, url, opts);
74
86
  }
75
87
  /** 走管理端 innerapi 的 GET + 信封解析 */
76
88
  async function getInnerApi(url, opts) {
77
- return handleInnerEnvelope(await getHttpClient().get(url), url, opts);
89
+ const startMs = logRequestStart("GET", url);
90
+ let response;
91
+ try {
92
+ response = await getHttpClient().get(url);
93
+ }
94
+ catch (err) {
95
+ logResponseFailure("GET", url, err, startMs);
96
+ throw err;
97
+ }
98
+ logResponseEnd("GET", url, response, startMs);
99
+ return handleInnerEnvelope(response, url, opts);
78
100
  }
79
101
  async function handleInnerEnvelope(response, url, opts) {
80
102
  if (!response.ok) {
@@ -85,6 +107,10 @@ async function handleInnerEnvelope(response, url, opts) {
85
107
  const env = result;
86
108
  if (env.status_code !== undefined && env.status_code !== "0") {
87
109
  const msg = env.message ?? env.error_msg ?? "unknown error";
110
+ // verbose: 把完整业务错误信封打到 stderr,便于追溯
111
+ if ((0, config_1.getConfig)().verbose) {
112
+ (0, logger_1.debug)(` envelope: ${truncateForLog(safeStringify(env), 1000)}`);
113
+ }
88
114
  const mapped = opts.mapErr?.(env.status_code, msg);
89
115
  if (mapped)
90
116
  throw mapped;
@@ -97,3 +123,63 @@ async function handleInnerEnvelope(response, url, opts) {
97
123
  }
98
124
  return result;
99
125
  }
126
+ // ── verbose 调试日志 ────────────
127
+ //
128
+ // 仅作用于 inner-api 链路(postInnerApi / getInnerApi),即 app / deploy /
129
+ // observability 三个域。--verbose 关闭时所有 helper 都直接退出,零开销。
130
+ function logRequestStart(method, url, body) {
131
+ const startMs = Date.now();
132
+ if (!(0, config_1.getConfig)().verbose)
133
+ return startMs;
134
+ (0, logger_1.debug)(`→ ${method} ${url}`);
135
+ if (body !== undefined) {
136
+ (0, logger_1.debug)(` body: ${truncateForLog(safeStringify(body), 1000)}`);
137
+ }
138
+ return startMs;
139
+ }
140
+ function logResponseEnd(method, url, response, startMs) {
141
+ if (!(0, config_1.getConfig)().verbose)
142
+ return;
143
+ const elapsedMs = Date.now() - startMs;
144
+ const logid = pickLogid(response.headers);
145
+ const tail = logid ? ` logid=${logid}` : "";
146
+ (0, logger_1.debug)(`← ${method} ${url} ${String(response.status)} ${String(elapsedMs)}ms${tail}`);
147
+ }
148
+ /**
149
+ * http-client 在 4xx/5xx / 网络错误时直接 reject(不返回 Response);
150
+ * 这条路径下 logResponseEnd 不会跑——这里专门补一行,尽可能从 HttpError
151
+ * 上抠出 status / logid,让 verbose 仍能看到失败请求的关键信息。
152
+ */
153
+ function logResponseFailure(method, url, err, startMs) {
154
+ if (!(0, config_1.getConfig)().verbose)
155
+ return;
156
+ const elapsedMs = Date.now() - startMs;
157
+ // @lark-apaas/http-client 的 HttpError.response: Response | undefined
158
+ const e = err;
159
+ const status = e.response?.status;
160
+ const logid = e.response?.headers ? pickLogid(e.response.headers) : null;
161
+ const statusPart = status !== undefined ? String(status) : "ERR";
162
+ const logidPart = logid ? ` logid=${logid}` : "";
163
+ const msgPart = e.message ? ` (${e.message})` : "";
164
+ (0, logger_1.debug)(`✗ ${method} ${url} ${statusPart} ${String(elapsedMs)}ms${logidPart}${msgPart}`);
165
+ }
166
+ /** BAM gateway 在不同 PSM/版本上 logid 落在不同 header;按命中顺序取第一个非空。 */
167
+ function pickLogid(headers) {
168
+ return (headers.get("x-tt-logid") ??
169
+ headers.get("x-logid") ??
170
+ headers.get("logid") ??
171
+ headers.get("x-tt-trace-tag"));
172
+ }
173
+ function safeStringify(v) {
174
+ try {
175
+ return JSON.stringify(v);
176
+ }
177
+ catch {
178
+ return String(v);
179
+ }
180
+ }
181
+ function truncateForLog(s, n) {
182
+ if (s.length <= n)
183
+ return s;
184
+ return `${s.slice(0, n)}...(truncated, ${String(s.length)} chars)`;
185
+ }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TIMESTAMP_HELP = void 0;
3
4
  exports.parseTimeToMs = parseTimeToMs;
4
5
  exports.msToNs = msToNs;
5
6
  exports.msToSec = msToSec;
@@ -9,16 +10,28 @@ exports.parseToSec = parseToSec;
9
10
  exports.floorMsToBucket = floorMsToBucket;
10
11
  exports.ceilMsToBucket = ceilMsToBucket;
11
12
  const error_1 = require("./error");
12
- const TIMESTAMP_HELP = "支持格式:相对时间 1h/2d/1w;日期 2026-04-01;ISO 2026-04-01T10:00:00Z";
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 拆成两个参数。";
13
19
  /**
14
- * 解析 PRD 时间格式。返回毫秒时间戳。
15
- * - 相对时间:`1h`、`2d`、`1w`、`30m`(h=小时, d=天, w=周, m=分钟)
16
- * - 日期:`2026-04-01`(按当日 00:00:00 UTC)
17
- * - ISO 8601:`2026-04-01T10:00:00Z`
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')、单独年份等。
18
30
  *
19
31
  * 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
20
32
  */
21
33
  function parseTimeToMs(input, now = new Date()) {
34
+ // 1. 相对时间:30m / 1h / 2d / 1w
22
35
  const relative = /^(\d+)([mhdw])$/.exec(input);
23
36
  if (relative) {
24
37
  const n = Number(relative[1]);
@@ -32,19 +45,77 @@ function parseTimeToMs(input, now = new Date()) {
32
45
  : /* w */ 604_800_000;
33
46
  return now.getTime() - n * factor;
34
47
  }
35
- if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
36
- const ms = Date.parse(input + "T00:00:00Z");
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);
37
70
  if (Number.isNaN(ms))
38
71
  failInvalidTimestamp(input);
39
72
  return ms;
40
73
  }
41
- const ms = Date.parse(input);
42
- if (Number.isNaN(ms))
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)
43
115
  failInvalidTimestamp(input);
44
- return ms;
45
116
  }
46
117
  function failInvalidTimestamp(input) {
47
- throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${TIMESTAMP_HELP}`);
118
+ throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${exports.TIMESTAMP_HELP}`);
48
119
  }
49
120
  /** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
50
121
  function msToNs(ms) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.2-alpha.c7d0c89",
3
+ "version": "0.1.2-alpha.ccfdc05",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {