@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.
Files changed (85) hide show
  1. package/README.md +8 -7
  2. package/dist/api/app/api.js +25 -0
  3. package/dist/api/app/index.js +15 -0
  4. package/dist/api/app/schemas.js +79 -0
  5. package/dist/api/app/types.js +58 -0
  6. package/dist/api/db/api.js +390 -55
  7. package/dist/api/db/client.js +65 -25
  8. package/dist/api/db/index.js +12 -1
  9. package/dist/api/db/parsers.js +20 -20
  10. package/dist/api/db/sql-keywords.js +87 -87
  11. package/dist/api/deploy/api.js +60 -0
  12. package/dist/api/deploy/index.js +16 -0
  13. package/dist/api/deploy/schemas.js +105 -0
  14. package/dist/api/deploy/types.js +22 -0
  15. package/dist/api/file/api.js +89 -87
  16. package/dist/api/file/client.js +62 -22
  17. package/dist/api/file/detect.js +3 -3
  18. package/dist/api/file/index.js +2 -1
  19. package/dist/api/file/parsers.js +18 -7
  20. package/dist/api/index.js +7 -1
  21. package/dist/api/observability/api.js +52 -0
  22. package/dist/api/observability/index.js +16 -0
  23. package/dist/api/observability/schemas.js +60 -0
  24. package/dist/api/observability/types.js +27 -0
  25. package/dist/api/plugin/api.js +31 -31
  26. package/dist/cli/commands/app/index.js +62 -0
  27. package/dist/cli/commands/db/index.js +600 -59
  28. package/dist/cli/commands/deploy/index.js +155 -0
  29. package/dist/cli/commands/file/index.js +91 -63
  30. package/dist/cli/commands/index.js +10 -6
  31. package/dist/cli/commands/observability/index.js +240 -0
  32. package/dist/cli/commands/plugin/index.js +27 -27
  33. package/dist/cli/commands/shared.js +86 -10
  34. package/dist/cli/handlers/app/get.js +47 -0
  35. package/dist/cli/handlers/app/index.js +7 -0
  36. package/dist/cli/handlers/app/update.js +59 -0
  37. package/dist/cli/handlers/db/_operator.js +35 -0
  38. package/dist/cli/handlers/db/audit.js +383 -0
  39. package/dist/cli/handlers/db/changelog.js +160 -0
  40. package/dist/cli/handlers/db/data.js +34 -34
  41. package/dist/cli/handlers/db/index.js +17 -1
  42. package/dist/cli/handlers/db/migration.js +245 -0
  43. package/dist/cli/handlers/db/quota.js +68 -0
  44. package/dist/cli/handlers/db/recovery.js +387 -0
  45. package/dist/cli/handlers/db/schema.js +35 -36
  46. package/dist/cli/handlers/db/sql.js +70 -71
  47. package/dist/cli/handlers/deploy/deploy.js +84 -0
  48. package/dist/cli/handlers/deploy/error-log.js +60 -0
  49. package/dist/cli/handlers/deploy/format.js +39 -0
  50. package/dist/cli/handlers/deploy/get.js +71 -0
  51. package/dist/cli/handlers/deploy/helpers.js +41 -0
  52. package/dist/cli/handlers/deploy/history.js +70 -0
  53. package/dist/cli/handlers/deploy/index.js +14 -0
  54. package/dist/cli/handlers/deploy/polling.js +162 -0
  55. package/dist/cli/handlers/file/cp.js +31 -32
  56. package/dist/cli/handlers/file/index.js +3 -1
  57. package/dist/cli/handlers/file/ls.js +6 -7
  58. package/dist/cli/handlers/file/quota.js +66 -0
  59. package/dist/cli/handlers/file/rm.js +33 -32
  60. package/dist/cli/handlers/file/sign.js +4 -5
  61. package/dist/cli/handlers/file/stat.js +11 -11
  62. package/dist/cli/handlers/observability/analytics.js +212 -0
  63. package/dist/cli/handlers/observability/helpers.js +66 -0
  64. package/dist/cli/handlers/observability/index.js +12 -0
  65. package/dist/cli/handlers/observability/log.js +94 -0
  66. package/dist/cli/handlers/observability/metric.js +208 -0
  67. package/dist/cli/handlers/observability/trace.js +102 -0
  68. package/dist/cli/handlers/plugin/plugin-local.js +53 -53
  69. package/dist/cli/handlers/plugin/plugin.js +15 -15
  70. package/dist/cli/help.js +16 -16
  71. package/dist/main.js +13 -9
  72. package/dist/utils/args.js +8 -0
  73. package/dist/utils/colors.js +2 -2
  74. package/dist/utils/config.js +2 -2
  75. package/dist/utils/devops-error.js +28 -0
  76. package/dist/utils/error.js +2 -2
  77. package/dist/utils/git.js +29 -0
  78. package/dist/utils/http.js +119 -1
  79. package/dist/utils/index.js +15 -1
  80. package/dist/utils/output.js +373 -20
  81. package/dist/utils/poll.js +35 -0
  82. package/dist/utils/render.js +27 -27
  83. package/dist/utils/spinner.js +46 -0
  84. package/dist/utils/time.js +208 -0
  85. package/package.json +7 -5
@@ -40,7 +40,6 @@ exports.handleFileRm = handleFileRm;
40
40
  const api = __importStar(require("../../../api/index"));
41
41
  const output_1 = require("../../../utils/output");
42
42
  const error_1 = require("../../../utils/error");
43
- const shared_1 = require("../../../cli/commands/shared");
44
43
  const index_1 = require("../../../api/file/index");
45
44
  const render_1 = require("../../../utils/render");
46
45
  const colors_1 = require("../../../utils/colors");
@@ -74,7 +73,7 @@ async function resolveDeleteInputs(appId, paths, names) {
74
73
  // 1a. path 分支:直接进删除队列,file_name 取 basename 先占位
75
74
  for (const p of positionalAsPath) {
76
75
  const abs = (0, index_1.toAbsolutePath)(p);
77
- const basename = abs.split("/").pop() ?? abs;
76
+ const basename = abs.split('/').pop() ?? abs;
78
77
  resolved.push({ input: p, path: abs, file_name: basename });
79
78
  }
80
79
  // 1b. name 分支(来自位置参数的非 path):和 --name 合并走 resolveInputs
@@ -83,10 +82,10 @@ async function resolveDeleteInputs(appId, paths, names) {
83
82
  const results = await api.file.resolveInputs({
84
83
  appId,
85
84
  inputs: allNames,
86
- forceAs: "name",
85
+ forceAs: 'name',
87
86
  });
88
87
  for (const r of results) {
89
- if (r.status === "ok") {
88
+ if (r.status === 'ok') {
90
89
  resolved.push({
91
90
  input: r.input,
92
91
  path: r.file.path,
@@ -95,7 +94,7 @@ async function resolveDeleteInputs(appId, paths, names) {
95
94
  }
96
95
  else {
97
96
  errors.push({
98
- status: "error",
97
+ status: 'error',
99
98
  input: r.input,
100
99
  error: r.error,
101
100
  });
@@ -105,11 +104,11 @@ async function resolveDeleteInputs(appId, paths, names) {
105
104
  return { resolved, errors };
106
105
  }
107
106
  /**
108
- * 删除前 TTY 二次确认。
109
- * PRD 单文件场景下提示带具体路径:`? Delete '/path'? (y/N)`,
110
- * 多文件场景汇总条数:`? Delete N files? (y/N)`。
111
- * `firstInput` 是用户传入的第一个值(可能是 path 或 file_name),
112
- * 单文件时直接展示给用户,方便核对目标。
107
+ * 删除前 TTY 二次确认。文案强调"permanently"(删除不可撤销,无回收站):
108
+ * - 单文件:`? Are you sure you want to permanently delete '<input>'? (y/N)`
109
+ * - 多文件:`? Are you sure you want to permanently delete N files? (y/N)`
110
+ * `firstInput` 是用户传入的第一个值(path 或 file_name),单文件时直接展示给
111
+ * 用户核对目标。
113
112
  */
114
113
  async function confirm(count, firstInput) {
115
114
  const rl = node_readline_1.default.createInterface({
@@ -117,10 +116,12 @@ async function confirm(count, firstInput) {
117
116
  output: process.stderr,
118
117
  });
119
118
  return new Promise((resolve) => {
120
- const prompt = count === 1 ? `? Delete '${firstInput}'? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
119
+ const prompt = count === 1
120
+ ? `? Are you sure you want to permanently delete '${firstInput}'? (y/N) `
121
+ : `? Are you sure you want to permanently delete ${String(count)} files? (y/N) `;
121
122
  rl.question(prompt, (answer) => {
122
123
  rl.close();
123
- resolve(answer.trim().toLowerCase() === "y");
124
+ resolve(answer.trim().toLowerCase() === 'y');
124
125
  });
125
126
  });
126
127
  }
@@ -128,12 +129,12 @@ async function handleFileRm(paths, opts) {
128
129
  const names = opts.name ?? [];
129
130
  const totalCount = paths.length + names.length;
130
131
  if (totalCount === 0) {
131
- throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
132
+ throw new error_1.AppError('ARGS_INVALID', 'No file specified (give a /path or --name <name>)');
132
133
  }
133
134
  if (totalCount > MAX_BATCH) {
134
- throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
135
+ throw new error_1.AppError('FILE_BATCH_TOO_MANY', `Batch size ${String(totalCount)} exceeds the 100 limit`);
135
136
  }
136
- const appId = (0, shared_1.resolveAppId)(opts);
137
+ const appId = opts.appId;
137
138
  // destructive guardrail
138
139
  const tty = (0, render_1.isStdoutTty)();
139
140
  if (tty && !opts.yes) {
@@ -142,11 +143,11 @@ async function handleFileRm(paths, opts) {
142
143
  const firstInput = paths.length > 0 ? paths[0] : names[0];
143
144
  const ok = await confirm(totalCount, firstInput);
144
145
  if (!ok) {
145
- throw new error_1.AppError("DESTRUCTIVE_CANCELLED", "Cancelled by user");
146
+ throw new error_1.AppError('DESTRUCTIVE_CANCELLED', 'Cancelled by user');
146
147
  }
147
148
  }
148
149
  else if (!tty && !opts.yes) {
149
- throw new error_1.AppError("DESTRUCTIVE_REQUIRES_CONFIRM", "This operation is destructive. Rerun with --yes to confirm.");
150
+ throw new error_1.AppError('DESTRUCTIVE_REQUIRES_CONFIRM', 'This operation is destructive. Rerun with --yes to confirm.');
150
151
  }
151
152
  const { resolved, errors } = await resolveDeleteInputs(appId, paths, names);
152
153
  // path → entry 映射(用于 delete 响应归档回原始 input 和 file_name)
@@ -168,9 +169,9 @@ async function handleFileRm(paths, opts) {
168
169
  for (const p of deleted) {
169
170
  const entry = pathToEntry.get(p);
170
171
  results.push({
171
- status: "ok",
172
+ status: 'ok',
172
173
  input: entry?.input ?? p,
173
- file_name: entry?.file_name ?? p.split("/").pop() ?? p,
174
+ file_name: entry?.file_name ?? p.split('/').pop() ?? p,
174
175
  path: p,
175
176
  });
176
177
  }
@@ -182,31 +183,31 @@ async function handleFileRm(paths, opts) {
182
183
  await api.file.statFile({ appId, filePath: f.path });
183
184
  return {
184
185
  input,
185
- code: "INTERNAL_ERROR",
186
- message: "Delete request returned success but file still exists (server-side issue)",
186
+ code: 'INTERNAL_ERROR',
187
+ message: 'Delete request returned success but file still exists (server-side issue)',
187
188
  hint: undefined,
188
189
  };
189
190
  }
190
191
  catch (err) {
191
- if (err instanceof error_1.AppError && err.code === "FILE_NOT_FOUND") {
192
+ if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
192
193
  return {
193
194
  input,
194
- code: "FILE_NOT_FOUND",
195
+ code: 'FILE_NOT_FOUND',
195
196
  message: `File '${input}' does not exist at delete time`,
196
- hint: "Run `miaoda file ls` to see available files.",
197
+ hint: 'Run `miaoda file ls` to see available files.',
197
198
  };
198
199
  }
199
200
  return {
200
201
  input,
201
- code: "INTERNAL_ERROR",
202
- message: err instanceof Error ? err.message : "verification failed",
202
+ code: 'INTERNAL_ERROR',
203
+ message: err instanceof Error ? err.message : 'verification failed',
203
204
  hint: undefined,
204
205
  };
205
206
  }
206
207
  }));
207
208
  for (const c of classified) {
208
209
  results.push({
209
- status: "error",
210
+ status: 'error',
210
211
  input: c.input,
211
212
  error: c.hint
212
213
  ? { code: c.code, message: c.message, hint: c.hint }
@@ -220,7 +221,7 @@ async function handleFileRm(paths, opts) {
220
221
  names.forEach((n, i) => orderIndex.set(n, paths.length + i));
221
222
  results = results.sort((a, b) => (orderIndex.get(a.input) ?? Number.MAX_SAFE_INTEGER) -
222
223
  (orderIndex.get(b.input) ?? Number.MAX_SAFE_INTEGER));
223
- const okCount = results.filter((r) => r.status === "ok").length;
224
+ const okCount = results.filter((r) => r.status === 'ok').length;
224
225
  const failCount = results.length - okCount;
225
226
  if ((0, output_1.isJsonMode)()) {
226
227
  (0, output_1.emit)({ data: results });
@@ -228,7 +229,7 @@ async function handleFileRm(paths, opts) {
228
229
  else if (tty) {
229
230
  const lines = [];
230
231
  for (const r of results) {
231
- if (r.status === "ok")
232
+ if (r.status === 'ok')
232
233
  lines.push(colors_1.c.success(`✓ Deleted ${r.input}`));
233
234
  else
234
235
  lines.push(colors_1.c.fail(`✗ ${r.input}: ${r.error.message}`));
@@ -239,12 +240,12 @@ async function handleFileRm(paths, opts) {
239
240
  else {
240
241
  lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
241
242
  }
242
- (0, output_1.emit)(lines.join("\n"));
243
+ (0, output_1.emit)(lines.join('\n'));
243
244
  }
244
245
  else {
245
246
  const lines = [];
246
247
  for (const r of results) {
247
- if (r.status === "ok")
248
+ if (r.status === 'ok')
248
249
  lines.push(`OK\t${r.input}`);
249
250
  else
250
251
  lines.push(`FAIL\t${r.input}\t${r.error.message}`);
@@ -255,7 +256,7 @@ async function handleFileRm(paths, opts) {
255
256
  else {
256
257
  lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
257
258
  }
258
- (0, output_1.emit)(lines.join("\n"));
259
+ (0, output_1.emit)(lines.join('\n'));
259
260
  }
260
261
  // 退出码:任一失败 → 1;全成功 → 0
261
262
  if (failCount > 0) {
@@ -38,7 +38,6 @@ const api = __importStar(require("../../../api/index"));
38
38
  const output_1 = require("../../../utils/output");
39
39
  const render_1 = require("../../../utils/render");
40
40
  const error_1 = require("../../../utils/error");
41
- const shared_1 = require("../../../cli/commands/shared");
42
41
  const index_1 = require("../../../api/file/index");
43
42
  const MAX_EXPIRES_SECONDS = 30 * 86400;
44
43
  const DEFAULT_EXPIRES_SECONDS = 86400; // 1d(PRD 规定)
@@ -54,9 +53,9 @@ async function resolveFilePath(appId, input) {
54
53
  const [resolved] = await api.file.resolveInputs({
55
54
  appId,
56
55
  inputs: [input],
57
- forceAs: "name",
56
+ forceAs: 'name',
58
57
  });
59
- if (resolved.status === "error") {
58
+ if (resolved.status === 'error') {
60
59
  throw new error_1.AppError(resolved.error.code, resolved.error.message, {
61
60
  next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
62
61
  });
@@ -64,12 +63,12 @@ async function resolveFilePath(appId, input) {
64
63
  return resolved.file.path;
65
64
  }
66
65
  async function handleFileSign(file, opts) {
67
- const appId = (0, shared_1.resolveAppId)(opts);
66
+ const appId = opts.appId;
68
67
  let expiresSec = DEFAULT_EXPIRES_SECONDS;
69
68
  if (opts.expires) {
70
69
  expiresSec = (0, render_1.parseDuration)(opts.expires);
71
70
  if (expiresSec > MAX_EXPIRES_SECONDS) {
72
- throw new error_1.AppError("FILE_SIGN_DURATION_EXCEEDED", `Expires duration '${opts.expires}' exceeds the maximum of 30d`, {
71
+ throw new error_1.AppError('FILE_SIGN_DURATION_EXCEEDED', `Expires duration '${opts.expires}' exceeds the maximum of 30d`, {
73
72
  next_actions: [
74
73
  `Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.`,
75
74
  ],
@@ -38,7 +38,6 @@ const api = __importStar(require("../../../api/index"));
38
38
  const output_1 = require("../../../utils/output");
39
39
  const render_1 = require("../../../utils/render");
40
40
  const error_1 = require("../../../utils/error");
41
- const shared_1 = require("../../../cli/commands/shared");
42
41
  const index_1 = require("../../../api/file/index");
43
42
  /**
44
43
  * 解析 `<file>` 为后端 head 可用的 filePath。
@@ -53,9 +52,9 @@ async function resolveFile(appId, input) {
53
52
  const [resolved] = await api.file.resolveInputs({
54
53
  appId,
55
54
  inputs: [input],
56
- forceAs: "name",
55
+ forceAs: 'name',
57
56
  });
58
- if (resolved.status === "error") {
57
+ if (resolved.status === 'error') {
59
58
  throw new error_1.AppError(resolved.error.code, resolved.error.message, {
60
59
  next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
61
60
  });
@@ -67,7 +66,7 @@ async function resolveFile(appId, input) {
67
66
  return head;
68
67
  }
69
68
  async function handleFileStat(file, opts) {
70
- const appId = (0, shared_1.resolveAppId)(opts);
69
+ const appId = opts.appId;
71
70
  const info = await resolveFile(appId, file);
72
71
  if ((0, output_1.isJsonMode)()) {
73
72
  (0, output_1.emitOk)(info);
@@ -75,23 +74,24 @@ async function handleFileStat(file, opts) {
75
74
  }
76
75
  const tty = (0, render_1.isStdoutTty)();
77
76
  const pairs = [
78
- ["file_name", info.file_name || ""],
79
- ["path", info.path],
77
+ ['file_name', info.file_name || ''],
78
+ ['path', info.path],
80
79
  [
81
- "size",
80
+ 'size',
82
81
  tty
83
82
  ? `${(0, render_1.formatSize)(info.size_bytes)} (${String(info.size_bytes)} bytes)`
84
83
  : String(info.size_bytes),
85
84
  ],
86
- ["type", info.type || ""],
87
- ["uploaded_at", (0, render_1.formatTime)(info.uploaded_at, tty)],
85
+ ['type', info.type || ''],
86
+ ['uploaded_at', (0, render_1.formatTime)(info.uploaded_at, tty)],
88
87
  ];
89
88
  if (info.uploaded_by) {
89
+ // pretty 模式只展示 name;id 仅在 --json 下保留(避免 TTY 输出冒出一长串 userID)。
90
90
  // uploaded_by 紧跟 uploaded_at 前插入(index = "uploaded_at" 之前)
91
- pairs.splice(pairs.length - 1, 0, ["uploaded_by", info.uploaded_by]);
91
+ pairs.splice(pairs.length - 1, 0, ['uploaded_by', info.uploaded_by.name]);
92
92
  }
93
93
  if (info.download_url) {
94
- pairs.push(["download_url", info.download_url]);
94
+ pairs.push(['download_url', info.download_url]);
95
95
  }
96
96
  (0, output_1.emit)((0, render_1.renderKeyValue)(pairs, tty));
97
97
  }
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.handleObservabilityAnalytics = handleObservabilityAnalytics;
37
+ const api = __importStar(require("../../../api/index"));
38
+ const output_1 = require("../../../utils/output");
39
+ const args_1 = require("../../../utils/args");
40
+ const helpers_1 = require("./helpers");
41
+ const GRANULARITY_TO_UNIT = {
42
+ day: 'DAY',
43
+ daily: 'DAY',
44
+ '1d': 'DAY',
45
+ '1h': 'HOUR',
46
+ hour: 'HOUR',
47
+ week: 'WEEK',
48
+ weekly: 'WEEK',
49
+ '1w': 'WEEK',
50
+ month: 'MONTH',
51
+ monthly: 'MONTH',
52
+ '1M': 'MONTH',
53
+ };
54
+ /**
55
+ * <analytics-name> + --series → metricType → 表头列名(=对应 --series 取值)。
56
+ *
57
+ * - users 缺省返回 active/new/total 三条线(多线视图)。
58
+ * - page-view 当前 BAM 只有一个 PAGE_VIEW metricType;不同 series 通过 device_type 过滤区分;
59
+ * 表头 label 直接对应 --series 取值(all-view / desktop-view / mobile-view)。
60
+ */
61
+ const ANALYTICS_LABELS = {
62
+ users: {
63
+ ACTIVE_USER: 'active-users',
64
+ NEW_USER: 'new-users',
65
+ TOTAL_USER: 'total-users',
66
+ },
67
+ };
68
+ /** miaoda observability analytics <analytics-name> */
69
+ async function handleObservabilityAnalytics(opts) {
70
+ if (!opts.analyticsName)
71
+ (0, args_1.failArgs)('<analytics-name> 必填');
72
+ const appID = opts.appId;
73
+ const { metricTypes, labelByMetric, extraFilters } = resolveAnalyticsSelection(opts.analyticsName, opts.series);
74
+ const timeAggregationUnit = opts.granularity
75
+ ? (GRANULARITY_TO_UNIT[opts.granularity] ?? opts.granularity.toUpperCase())
76
+ : 'DAY';
77
+ const nowMs = Date.now();
78
+ const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
79
+ const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
80
+ const bucket = timeAggregationUnit;
81
+ const fieldFilters = (0, helpers_1.buildFieldFilters)([
82
+ { key: 'path', value: opts.page ? (0, helpers_1.eqFilter)(opts.page) : undefined },
83
+ ...extraFilters,
84
+ ]);
85
+ const req = {
86
+ appID,
87
+ metricTypes,
88
+ startTimestampNs: (0, helpers_1.msToNs)(sinceMs),
89
+ endTimestampNs: (0, helpers_1.msToNs)((0, helpers_1.ceilMsToBucket)(untilMs, bucket)),
90
+ timeAggregationUnit,
91
+ fieldFilters,
92
+ };
93
+ const resp = await api.observability.queryAnalyticsData(req);
94
+ emitAnalyticsResponse(resp, labelByMetric);
95
+ }
96
+ // ── 渲染 ────────────
97
+ //
98
+ // BAM v1.0.123 起返回 points: [{timestampNs, dimensions, values: {metricType: v}}]——
99
+ // 服务端已把多 metricType 合并到同 timestampNs 的 row,下游直接平铺即可。
100
+ // JSON 模式:原样透传 points(保留 BAM 原 metricType 作为 values 的 key)。
101
+ // Pretty 模式:把 values 里的 metricType 重命名成 CLI label(active-users 等),
102
+ // 再走 buildAnalyticsPivotSchema 渲染。
103
+ function emitAnalyticsResponse(resp, labelByMetric) {
104
+ const points = resp.points ?? [];
105
+ // analytics 不分页,但补齐 has_more / next_cursor 与 log/trace 信封形状对齐
106
+ if ((0, output_1.isJsonMode)()) {
107
+ (0, output_1.emit)({ data: points, next_cursor: null, has_more: false });
108
+ return;
109
+ }
110
+ const { rows, seriesLabels } = renameAndSort(points, labelByMetric);
111
+ if (rows.length === 0) {
112
+ (0, output_1.emit)({ data: [], next_cursor: null, has_more: false });
113
+ return;
114
+ }
115
+ (0, output_1.emit)({ data: rows, next_cursor: null, has_more: false }, buildAnalyticsPivotSchema(seriesLabels));
116
+ }
117
+ function renameAndSort(points, labelByMetric) {
118
+ // 列顺序按 labelByMetric 声明顺序,避免 BAM 返回顺序波动影响表头
119
+ const seriesLabels = Object.values(labelByMetric);
120
+ const rows = points.map((point) => {
121
+ const row = { timestampNs: point.timestampNs };
122
+ for (const [metricType, value] of Object.entries(point.values)) {
123
+ const label = labelByMetric[metricType] ?? metricType;
124
+ row[label] = value;
125
+ }
126
+ return row;
127
+ });
128
+ rows.sort((a, b) => b.timestampNs - a.timestampNs);
129
+ return { rows, seriesLabels };
130
+ }
131
+ function buildAnalyticsPivotSchema(seriesLabels) {
132
+ return {
133
+ columns: [
134
+ { key: 'timestampNs', label: 'time', format: output_1.fmt.ns() },
135
+ ...seriesLabels.map((label) => ({ key: label, label })),
136
+ ],
137
+ strict: true,
138
+ };
139
+ }
140
+ /**
141
+ * <analytics-name> + --series → metricTypes[] + label 映射 + 附加过滤
142
+ *
143
+ * users:
144
+ * - 缺省 series → 三条线全返回(active-users/new-users/total-users)
145
+ * - 单值 series → 对应单条线
146
+ * page-view:
147
+ * - 缺省/all-view → 单条 PAGE_VIEW,无 device 过滤,label = "all-view"
148
+ * - desktop-view / mobile-view → 单条 PAGE_VIEW + device_type 过滤,label = --series 值
149
+ */
150
+ function resolveAnalyticsSelection(cliName, series) {
151
+ const extras = [];
152
+ const normalizedSeries = normalizeAnalyticsSeries(cliName, series);
153
+ if (cliName === 'users') {
154
+ if (normalizedSeries === 'active-users')
155
+ return single('ACTIVE_USER', 'active-users', extras);
156
+ if (normalizedSeries === 'new-users')
157
+ return single('NEW_USER', 'new-users', extras);
158
+ if (normalizedSeries === 'total-users')
159
+ return single('TOTAL_USER', 'total-users', extras);
160
+ // 缺省:三条线
161
+ return all(ANALYTICS_LABELS.users, extras);
162
+ }
163
+ if (cliName === 'page-view') {
164
+ if (normalizedSeries === 'desktop-view') {
165
+ extras.push({ key: 'device_type', value: (0, helpers_1.eqFilter)('desktop') });
166
+ return single('PAGE_VIEW', 'desktop-view', extras);
167
+ }
168
+ if (normalizedSeries === 'mobile-view') {
169
+ extras.push({ key: 'device_type', value: (0, helpers_1.eqFilter)('mobile') });
170
+ return single('PAGE_VIEW', 'mobile-view', extras);
171
+ }
172
+ // 缺省 / all-view → 不附加 device 过滤
173
+ return single('PAGE_VIEW', 'all-view', extras);
174
+ }
175
+ // 兜底:CLI 名直接当 metricType + label
176
+ return single(cliName, cliName, extras);
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
+ }
199
+ function single(metricType, label, extras) {
200
+ return {
201
+ metricTypes: [metricType],
202
+ labelByMetric: { [metricType]: label },
203
+ extraFilters: extras,
204
+ };
205
+ }
206
+ function all(labelByMetric, extras) {
207
+ return {
208
+ metricTypes: Object.keys(labelByMetric),
209
+ labelByMetric,
210
+ extraFilters: extras,
211
+ };
212
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ceilMsToBucket = exports.floorMsToBucket = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = void 0;
4
+ exports.eqFilter = eqFilter;
5
+ exports.rangeFilter = rangeFilter;
6
+ exports.fuzzyFilter = fuzzyFilter;
7
+ exports.buildFieldFilters = buildFieldFilters;
8
+ exports.validateLimit = validateLimit;
9
+ const args_1 = require("../../../utils/args");
10
+ const index_1 = require("../../../api/observability/index");
11
+ // 时间解析助手已抽到 utils/time.ts;这里 re-export 维持 observability handler 的旧 import 路径。
12
+ var time_1 = require("../../../utils/time");
13
+ Object.defineProperty(exports, "parseTimeToMs", { enumerable: true, get: function () { return time_1.parseTimeToMs; } });
14
+ Object.defineProperty(exports, "parseToMs", { enumerable: true, get: function () { return time_1.parseToMs; } });
15
+ Object.defineProperty(exports, "parseToNs", { enumerable: true, get: function () { return time_1.parseToNs; } });
16
+ Object.defineProperty(exports, "parseToSec", { enumerable: true, get: function () { return time_1.parseToSec; } });
17
+ Object.defineProperty(exports, "msToNs", { enumerable: true, get: function () { return time_1.msToNs; } });
18
+ Object.defineProperty(exports, "msToSec", { enumerable: true, get: function () { return time_1.msToSec; } });
19
+ Object.defineProperty(exports, "floorMsToBucket", { enumerable: true, get: function () { return time_1.floorMsToBucket; } });
20
+ Object.defineProperty(exports, "ceilMsToBucket", { enumerable: true, get: function () { return time_1.ceilMsToBucket; } });
21
+ // ── filter 构造 ──
22
+ //
23
+ // BAM FieldFilter 是「类型分桶 + 算子」结构(str/i64/double),不是扁平字段;
24
+ // 字符串字段无 server-side contains,模糊匹配走 fuzzyFilter。
25
+ /** 字符串字段等值过滤 → { str: { eq } } */
26
+ function eqFilter(value, type = 'str') {
27
+ const v = type !== 'str' ? Number(value) : value;
28
+ return { [type]: { eq: v } };
29
+ }
30
+ /** i64 数值范围过滤 → { i64: { gte, lte } };输入接受 string(CLI flag)或 number */
31
+ function rangeFilter(opts) {
32
+ const i64 = {};
33
+ if (opts.gte !== undefined)
34
+ i64.gte = Number(opts.gte);
35
+ if (opts.lte !== undefined)
36
+ i64.lte = Number(opts.lte);
37
+ return { i64 };
38
+ }
39
+ /** 模糊搜索过滤器;默认 AND 运算符 */
40
+ function fuzzyFilter(keyword, operator = index_1.WordOperator.AND) {
41
+ return { include: { words: [keyword], operator } };
42
+ }
43
+ /**
44
+ * 把若干条件按 key 装到 fieldFilters。值为 undefined 的条目跳过;
45
+ * 全部跳过时返回 undefined(避免传空 map)。
46
+ */
47
+ function buildFieldFilters(entries) {
48
+ const out = {};
49
+ let added = false;
50
+ for (const { key, value } of entries) {
51
+ if (value === undefined)
52
+ continue;
53
+ out[key] = value;
54
+ added = true;
55
+ }
56
+ return added ? out : undefined;
57
+ }
58
+ // ── 校验 ──
59
+ /** limit 校验(API 限制 1~100) */
60
+ function validateLimit(limit, defaultLimit = 50) {
61
+ const n = Number.isFinite(limit) ? limit : defaultLimit;
62
+ if (n <= 0 || n > 100) {
63
+ (0, args_1.failArgs)(`--limit 必须在 1~100 之间,收到 ${String(n)}`);
64
+ }
65
+ return n;
66
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleObservabilityAnalytics = exports.handleObservabilityMetric = exports.handleObservabilityTraceGet = exports.handleObservabilityTraceList = exports.handleObservabilityLog = void 0;
4
+ var log_1 = require("./log");
5
+ Object.defineProperty(exports, "handleObservabilityLog", { enumerable: true, get: function () { return log_1.handleObservabilityLog; } });
6
+ var trace_1 = require("./trace");
7
+ Object.defineProperty(exports, "handleObservabilityTraceList", { enumerable: true, get: function () { return trace_1.handleObservabilityTraceList; } });
8
+ Object.defineProperty(exports, "handleObservabilityTraceGet", { enumerable: true, get: function () { return trace_1.handleObservabilityTraceGet; } });
9
+ var metric_1 = require("./metric");
10
+ Object.defineProperty(exports, "handleObservabilityMetric", { enumerable: true, get: function () { return metric_1.handleObservabilityMetric; } });
11
+ var analytics_1 = require("./analytics");
12
+ Object.defineProperty(exports, "handleObservabilityAnalytics", { enumerable: true, get: function () { return analytics_1.handleObservabilityAnalytics; } });