@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
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.errorJobSchema = exports.deployGetSchema = exports.deployHistorySchema = void 0;
4
+ exports.nodeStatusText = nodeStatusText;
5
+ exports.nodeStatusFromText = nodeStatusFromText;
6
+ const output_1 = require("../../utils/output");
7
+ const types_1 = require("./types");
8
+ const NODE_STATUS_TEXT = {
9
+ [types_1.NodeStatus.UNSPECIFIED]: 'unknown',
10
+ [types_1.NodeStatus.TODO]: 'todo',
11
+ [types_1.NodeStatus.RUNNING]: 'running',
12
+ [types_1.NodeStatus.SUCCESS]: 'success',
13
+ [types_1.NodeStatus.FAILED]: 'failed',
14
+ [types_1.NodeStatus.CANCELED]: 'canceled',
15
+ [types_1.NodeStatus.HOLD_ON]: 'hold_on',
16
+ };
17
+ function nodeStatusText(v) {
18
+ if (typeof v === 'string')
19
+ return v;
20
+ if (typeof v !== 'number')
21
+ return undefined;
22
+ return NODE_STATUS_TEXT[v];
23
+ }
24
+ /** NodeStatus 文本(CLI flag 字符串)→ 枚举数值 */
25
+ function nodeStatusFromText(text) {
26
+ const lower = text.toLowerCase();
27
+ for (const [num, label] of Object.entries(NODE_STATUS_TEXT)) {
28
+ if (label === lower)
29
+ return Number(num);
30
+ }
31
+ return undefined;
32
+ }
33
+ function pipelineDurationMs(row) {
34
+ // SimpleInstance.createdAt / updatedAt:BAM IDL 未注单位,e2e 看是 ms。
35
+ const start = Number(row.createdAt);
36
+ const end = Number(row.updatedAt);
37
+ if (!Number.isFinite(start) || !Number.isFinite(end))
38
+ return undefined;
39
+ return end - start;
40
+ }
41
+ /**
42
+ * deploy history 列表的 pretty 渲染契约。
43
+ *
44
+ * BAM 返回 SimpleInstance(pipeline 实例),状态走 NodeStatus。
45
+ * 默认列:deploy-id(=ID)/ status / creator / created-at / duration(updatedAt-createdAt)。
46
+ */
47
+ exports.deployHistorySchema = {
48
+ columns: [
49
+ { key: 'ID', label: 'deploy-id' },
50
+ {
51
+ key: 'status_text',
52
+ label: 'status',
53
+ derive: (row) => nodeStatusText(row.status),
54
+ },
55
+ { key: 'creator', label: 'creator' },
56
+ { key: 'createdAt', label: 'created-at', format: output_1.fmt.ms() },
57
+ {
58
+ key: 'duration',
59
+ label: 'duration',
60
+ format: output_1.fmt.durationMs(),
61
+ derive: pipelineDurationMs,
62
+ },
63
+ ],
64
+ strict: true,
65
+ };
66
+ /**
67
+ * deploy get 单条详情的 key-value 渲染契约(基于 SimpleInstance)。
68
+ *
69
+ * 由于 BAM 没有 by-ID 的查询接口,handler 用 listPipelineInstances 在最近窗口
70
+ * 里筛一条 SimpleInstance 透传过来。
71
+ */
72
+ exports.deployGetSchema = {
73
+ columns: [
74
+ { key: 'ID', label: 'deploy-id' },
75
+ {
76
+ key: 'status_text',
77
+ label: 'status',
78
+ derive: (row) => nodeStatusText(row.status),
79
+ },
80
+ { key: 'creator', label: 'creator' },
81
+ { key: 'updater', label: 'updater' },
82
+ { key: 'createdAt', label: 'created-at', format: output_1.fmt.ms() },
83
+ { key: 'updatedAt', label: 'updated-at', format: output_1.fmt.ms() },
84
+ {
85
+ key: 'duration',
86
+ label: 'duration',
87
+ format: output_1.fmt.durationMs(),
88
+ derive: pipelineDurationMs,
89
+ },
90
+ { key: 'templateID', label: 'template' },
91
+ { key: 'description' },
92
+ { key: 'parameters' },
93
+ { key: 'envVariables', label: 'env-vars' },
94
+ ],
95
+ strict: true,
96
+ };
97
+ /** deploy error-log 表格渲染契约 */
98
+ exports.errorJobSchema = {
99
+ columns: [
100
+ { key: 'jobID', label: 'job-id' },
101
+ { key: 'componentName', label: 'component' },
102
+ { key: 'errorMsg', label: 'error' },
103
+ ],
104
+ strict: true,
105
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ // 与 BAM lark.apaas.devops v1.0.326 / 1.0.327 + lark.apaas.devops_platform v1.0.247 对齐:
3
+ // - CLICreateForceRelease (4070318) 创建发布 lark.apaas.devops
4
+ // - CliQueryPipelineInstance (4069872) 轮询发布状态 lark.apaas.devops_platform
5
+ // - CliListPipelineInstances (4073969) 分页获取发布历史 lark.apaas.devops_platform
6
+ // - CliGetToolInstanceErrorJobs (4073972) 获取发布错误日志 lark.apaas.devops_platform
7
+ //
8
+ // 发布单的唯一标识是 pipelineTaskID,对外即 deploy-id;error-log 等接口直接用它做
9
+ // URL 路径段(instanceID)。曾经的 Release/releaseID 概念已全面废弃。
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.NodeStatus = void 0;
12
+ /** Pipeline 节点状态(devops_platform) */
13
+ var NodeStatus;
14
+ (function (NodeStatus) {
15
+ NodeStatus[NodeStatus["UNSPECIFIED"] = 0] = "UNSPECIFIED";
16
+ NodeStatus[NodeStatus["TODO"] = 1] = "TODO";
17
+ NodeStatus[NodeStatus["RUNNING"] = 2] = "RUNNING";
18
+ NodeStatus[NodeStatus["SUCCESS"] = 3] = "SUCCESS";
19
+ NodeStatus[NodeStatus["FAILED"] = 4] = "FAILED";
20
+ NodeStatus[NodeStatus["CANCELED"] = 5] = "CANCELED";
21
+ NodeStatus[NodeStatus["HOLD_ON"] = 6] = "HOLD_ON";
22
+ })(NodeStatus || (exports.NodeStatus = NodeStatus = {}));
@@ -8,8 +8,10 @@ exports.uploadFile = uploadFile;
8
8
  exports.signDownload = signDownload;
9
9
  exports.downloadFile = downloadFile;
10
10
  exports.deleteFiles = deleteFiles;
11
+ exports.getStorageQuota = getStorageQuota;
11
12
  const error_1 = require("../../utils/error");
12
13
  const logger_1 = require("../../utils/logger");
14
+ const time_1 = require("../../utils/time");
13
15
  const client_1 = require("./client");
14
16
  const detect_1 = require("./detect");
15
17
  const parsers_1 = require("./parsers");
@@ -19,18 +21,18 @@ const parsers_1 = require("./parsers");
19
21
  * 所有进入 API 层的 path 都要 strip 前导 `/`。
20
22
  */
21
23
  function toApiPath(filePath) {
22
- return filePath.replace(/^\/+/, "");
24
+ return filePath.replace(/^\/+/, '');
23
25
  }
24
26
  function encodePath(filePath) {
25
27
  return toApiPath(filePath)
26
- .split("/")
28
+ .split('/')
27
29
  .map((seg) => encodeURIComponent(seg))
28
- .join("/");
30
+ .join('/');
29
31
  }
30
32
  function extractEnvelope(body) {
31
33
  (0, client_1.ensureSuccess)(body);
32
34
  if (!body.data) {
33
- throw new error_1.AppError("INTERNAL_FILE_API_ERROR", "API returned empty data");
35
+ throw new error_1.AppError('INTERNAL_FILE_API_ERROR', 'API returned empty data');
34
36
  }
35
37
  return body.data;
36
38
  }
@@ -49,7 +51,7 @@ async function listOnce(appId, bucketId, opts) {
49
51
  if (opts.sortBy && opts.sortBy.length > 0)
50
52
  reqBody.sortBy = opts.sortBy;
51
53
  const body = await (0, client_1.doPost)(url, reqBody, {
52
- errorContext: "list files",
54
+ errorContext: 'list files',
53
55
  });
54
56
  return extractEnvelope(body);
55
57
  }
@@ -70,29 +72,29 @@ function buildFilterExpr(opts) {
70
72
  return opts.filterExpr;
71
73
  const conds = [];
72
74
  if (opts.name) {
73
- conds.push({ field: "name", operator: "eq", value: opts.name });
75
+ conds.push({ field: 'name', operator: 'eq', value: opts.name });
74
76
  }
75
77
  if (opts.type) {
76
- conds.push({ field: "type", operator: "eq", value: opts.type });
78
+ conds.push({ field: 'type', operator: 'eq', value: opts.type });
77
79
  }
78
80
  if (opts.sizeGt !== undefined) {
79
- conds.push({ field: "size", operator: "gt", value: String(opts.sizeGt) });
81
+ conds.push({ field: 'size', operator: 'gt', value: String(opts.sizeGt) });
80
82
  }
81
83
  if (opts.sizeLt !== undefined) {
82
- conds.push({ field: "size", operator: "lt", value: String(opts.sizeLt) });
84
+ conds.push({ field: 'size', operator: 'lt', value: String(opts.sizeLt) });
83
85
  }
84
86
  if (opts.uploadedSince) {
85
87
  // 后端期望毫秒 timestamp(strconv.ParseInt → time.UnixMilli)
86
- const ms = parseTimeFilterMs(opts.uploadedSince, "--uploaded-since");
87
- conds.push({ field: "createdAt", operator: "gte", value: String(ms) });
88
+ const ms = parseTimeFilterMs(opts.uploadedSince, '--uploaded-since');
89
+ conds.push({ field: 'createdAt', operator: 'gte', value: String(ms) });
88
90
  }
89
91
  if (opts.uploadedUntil) {
90
- const ms = parseTimeFilterMs(opts.uploadedUntil, "--uploaded-until");
91
- conds.push({ field: "createdAt", operator: "lte", value: String(ms) });
92
+ const ms = parseTimeFilterMs(opts.uploadedUntil, '--uploaded-until');
93
+ conds.push({ field: 'createdAt', operator: 'lte', value: String(ms) });
92
94
  }
93
95
  if (conds.length === 0)
94
96
  return undefined;
95
- return { logic: "and", groups: [{ conditions: conds }] };
97
+ return { logic: 'and', groups: [{ conditions: conds }] };
96
98
  }
97
99
  /**
98
100
  * 解析时间过滤参数(--uploaded-since / --uploaded-until)的三种输入格式
@@ -107,31 +109,17 @@ function buildFilterExpr(opts) {
107
109
  * flagName 用于错误信息,调用方传 "--uploaded-since" 或 "--uploaded-until"。
108
110
  */
109
111
  function parseTimeFilterMs(input, flagName) {
110
- // 相对时间:<positive int><unit>,单位 s/m/h/d/w
111
- const RELATIVE = /^(\d+)([smhdw])$/;
112
- const rel = RELATIVE.exec(input);
113
- if (rel) {
114
- const n = parseInt(rel[1], 10);
115
- if (n <= 0) {
116
- throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 必须是正整数 + 单位(s / m / h / d / w)`);
117
- }
118
- const UNIT_MS = {
119
- s: 1_000,
120
- m: 60_000,
121
- h: 3_600_000,
122
- d: 86_400_000,
123
- w: 604_800_000,
124
- };
125
- return Date.now() - n * UNIT_MS[rel[2]];
112
+ try {
113
+ return (0, time_1.parseTimeToMs)(input);
126
114
  }
127
- // 绝对时间:date / ISO 8601。Date.parse 对 "YYYY-MM-DD" 按 UTC 00:00:00 解析,
128
- // 对带 Z ISO 8601 也直接出 UTC ms。
129
- const ms = Date.parse(input);
130
- if (Number.isNaN(ms)) {
131
- throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 格式无法识别。支持:` +
132
- `相对时间(如 1h / 2d / 1w)、日期(YYYY-MM-DD)、ISO 8601(如 2026-04-01T10:00:00Z)`);
115
+ catch (err) {
116
+ if (err instanceof error_1.AppError) {
117
+ throw new error_1.AppError(err.code, `${flagName}: ${err.message}`, {
118
+ next_actions: err.next_actions,
119
+ });
120
+ }
121
+ throw err;
133
122
  }
134
- return ms;
135
123
  }
136
124
  /**
137
125
  * 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
@@ -147,7 +135,7 @@ async function listFiles(opts) {
147
135
  let hasMore = false;
148
136
  // 分页 cursor 用 raw attachment.id(后端 continuationToken 语义);
149
137
  // CLI 对外不暴露 attachment.id,所以这里单独追踪最后一页的 raw id
150
- let lastRawId = "";
138
+ let lastRawId = '';
151
139
  for (;;) {
152
140
  const fetchLimit = opts.all ? 200 : pageSize;
153
141
  const data = await listOnce(opts.appId, bucketId, {
@@ -159,7 +147,7 @@ async function listFiles(opts) {
159
147
  const rawItems = data.attachments ?? [];
160
148
  hasMore = Boolean(data.hasMore);
161
149
  if (rawItems.length > 0) {
162
- lastRawId = rawItems[rawItems.length - 1].id ?? "";
150
+ lastRawId = rawItems[rawItems.length - 1].id ?? '';
163
151
  }
164
152
  for (const raw of rawItems) {
165
153
  const info = (0, parsers_1.parseAttachment)(raw);
@@ -208,8 +196,8 @@ async function resolveInputs(opts) {
208
196
  if (opts.inputs.length === 0)
209
197
  return [];
210
198
  return Promise.all(opts.inputs.map((input) => {
211
- const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? "path" : "name");
212
- return kind === "path" ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
199
+ const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? 'path' : 'name');
200
+ return kind === 'path' ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
213
201
  }));
214
202
  }
215
203
  /** path 输入:HEAD 判存在性,path 在 bucket 内 unique 无需反查。 */
@@ -218,17 +206,17 @@ async function resolveByPath(appId, input) {
218
206
  const filePath = (0, detect_1.toAbsolutePath)(input);
219
207
  try {
220
208
  const info = await statFile({ appId, filePath });
221
- return { status: "ok", input, file: info };
209
+ return { status: 'ok', input, file: info };
222
210
  }
223
211
  catch (err) {
224
- if (err instanceof error_1.AppError && err.code === "FILE_NOT_FOUND") {
212
+ if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
225
213
  return {
226
- status: "error",
214
+ status: 'error',
227
215
  input,
228
216
  error: {
229
- code: "FILE_NOT_FOUND",
217
+ code: 'FILE_NOT_FOUND',
230
218
  message: `File '${input}' does not exist`,
231
- hint: "Run `miaoda file ls` to see available files.",
219
+ hint: 'Run `miaoda file ls` to see available files.',
232
220
  },
233
221
  };
234
222
  }
@@ -238,38 +226,38 @@ async function resolveByPath(appId, input) {
238
226
  /** file_name 输入:list + FilterExpression `name eq X`,多匹配 AMBIGUOUS。 */
239
227
  async function resolveByName(appId, input) {
240
228
  const filterExpr = {
241
- logic: "and",
242
- groups: [{ conditions: [{ field: "name", operator: "eq", value: input }] }],
229
+ logic: 'and',
230
+ groups: [{ conditions: [{ field: 'name', operator: 'eq', value: input }] }],
243
231
  };
244
232
  const result = await listFiles({ appId, filterExpr, limit: 200 });
245
233
  // 后端返 name 精确匹配的所有 attachments。basename fallback 防御(后端某些场景 name 可能为空)
246
234
  const matches = result.items.filter((i) => {
247
- const basename = i.path.split("/").pop() ?? i.path;
235
+ const basename = i.path.split('/').pop() ?? i.path;
248
236
  return i.file_name === input || basename === input;
249
237
  });
250
238
  if (matches.length === 0) {
251
239
  return {
252
- status: "error",
240
+ status: 'error',
253
241
  input,
254
242
  error: {
255
- code: "FILE_NOT_FOUND",
243
+ code: 'FILE_NOT_FOUND',
256
244
  message: `File '${input}' does not exist`,
257
- hint: "Run `miaoda file ls` to see available files.",
245
+ hint: 'Run `miaoda file ls` to see available files.',
258
246
  },
259
247
  };
260
248
  }
261
249
  if (matches.length > 1) {
262
250
  return {
263
- status: "error",
251
+ status: 'error',
264
252
  input,
265
253
  error: {
266
- code: "AMBIGUOUS_FILE_NAME",
254
+ code: 'AMBIGUOUS_FILE_NAME',
267
255
  message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
268
256
  hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
269
257
  },
270
258
  };
271
259
  }
272
- return { status: "ok", input, file: matches[0] };
260
+ return { status: 'ok', input, file: matches[0] };
273
261
  }
274
262
  // ── stat ──
275
263
  /**
@@ -281,10 +269,10 @@ async function statFile(opts) {
281
269
  // 对齐 file-storage-skill Py 版:URL 顺序是 .../object/{bucket}/head/{path}
282
270
  const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/${encodeURIComponent(bucketId)}/head/${encodePath(opts.filePath)}`;
283
271
  const body = await (0, client_1.doGet)(url, {
284
- notFoundCode: "FILE_NOT_FOUND",
272
+ notFoundCode: 'FILE_NOT_FOUND',
285
273
  notFoundMessage: `File '${opts.filePath}' does not exist`,
286
- notFoundHint: "Run `miaoda file ls` to see available files.",
287
- errorContext: "stat file",
274
+ notFoundHint: 'Run `miaoda file ls` to see available files.',
275
+ errorContext: 'stat file',
288
276
  });
289
277
  return (0, parsers_1.parseHead)(extractEnvelope(body), opts.filePath);
290
278
  }
@@ -292,7 +280,7 @@ async function statFile(opts) {
292
280
  async function preUpload(appId, req) {
293
281
  const bucketId = await (0, client_1.getDefaultBucketId)(appId);
294
282
  const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/preUpload`;
295
- const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: "pre-upload" });
283
+ const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: 'pre-upload' });
296
284
  return extractEnvelope(body);
297
285
  }
298
286
  /**
@@ -308,14 +296,14 @@ async function preUpload(appId, req) {
308
296
  async function uploadCallback(appId, req) {
309
297
  const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
310
298
  const body = await (0, client_1.doPost)(url, req, {
311
- errorContext: "upload callback",
299
+ errorContext: 'upload callback',
312
300
  });
313
301
  const data = extractEnvelope(body);
314
302
  const metadata = data.metadata;
315
303
  if (!metadata) {
316
304
  return {};
317
305
  }
318
- if (typeof metadata === "object") {
306
+ if (typeof metadata === 'object') {
319
307
  return metadata;
320
308
  }
321
309
  // string 形态兜底
@@ -342,10 +330,10 @@ async function uploadFile(opts) {
342
330
  filePath: opts.remotePath ? toApiPath(opts.remotePath) : undefined,
343
331
  });
344
332
  if (!pre.uploadURL || !pre.uploadID) {
345
- throw new error_1.AppError("FILE_UPLOAD_FAILED", "preUpload did not return uploadURL/uploadID");
333
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', 'preUpload did not return uploadURL/uploadID');
346
334
  }
347
335
  const body = await opts.readFile();
348
- let etag = "";
336
+ let etag = '';
349
337
  try {
350
338
  // Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
351
339
  // 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
@@ -355,27 +343,27 @@ async function uploadFile(opts) {
355
343
  // header 作为对象 metadata 存住,服务端 callback 阶段 HeadObject 读回并解析
356
344
  // filename 写入 DB。我们要不传 header,服务端走兜底会把 storage key 当文件名。
357
345
  const res = await fetch(pre.uploadURL, {
358
- method: "PUT",
346
+ method: 'PUT',
359
347
  headers: {
360
- "Content-Type": opts.contentType,
361
- "Content-Length": String(opts.fileSize),
362
- "Content-Disposition": `attachment; filename="${sanitizeFileName(opts.fileName)}"`,
348
+ 'Content-Type': opts.contentType,
349
+ 'Content-Length': String(opts.fileSize),
350
+ 'Content-Disposition': `attachment; filename="${sanitizeFileName(opts.fileName)}"`,
363
351
  },
364
352
  body: ab,
365
353
  });
366
354
  (0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
367
355
  if (!res.ok) {
368
- throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
356
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed with status ${String(res.status)}`);
369
357
  }
370
- etag = res.headers.get("ETag") ?? "";
358
+ etag = res.headers.get('ETag') ?? '';
371
359
  }
372
360
  catch (err) {
373
361
  if (err instanceof error_1.AppError)
374
362
  throw err;
375
- throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed: ${err instanceof Error ? err.message : String(err)}`);
363
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed: ${err instanceof Error ? err.message : String(err)}`);
376
364
  }
377
365
  if (!etag) {
378
- throw new error_1.AppError("FILE_UPLOAD_FAILED", "Upload PUT returned no ETag");
366
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', 'Upload PUT returned no ETag');
379
367
  }
380
368
  // callback 返回服务端实际生成的对象元数据(filePath / file_name / download_url)。
381
369
  // path 末段是平台生成的 16 位 GID,CLI 无法靠本地信息推断;callback 失败 /
@@ -389,20 +377,20 @@ async function uploadFile(opts) {
389
377
  }
390
378
  catch (err) {
391
379
  const reason = err instanceof Error ? err.message : String(err);
392
- throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", `Upload callback failed: ${reason}; file may already exist in storage`, {
380
+ throw new error_1.AppError('FILE_UPLOAD_CALLBACK_INCOMPLETE', `Upload callback failed: ${reason}; file may already exist in storage`, {
393
381
  next_actions: [
394
382
  `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
395
383
  ],
396
384
  });
397
385
  }
398
386
  if (!metadata.filePath) {
399
- throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", "Upload callback returned no filePath; file may already exist in storage", {
387
+ throw new error_1.AppError('FILE_UPLOAD_CALLBACK_INCOMPLETE', 'Upload callback returned no filePath; file may already exist in storage', {
400
388
  next_actions: [
401
389
  `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
402
390
  ],
403
391
  });
404
392
  }
405
- const path = metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath;
393
+ const path = metadata.filePath.startsWith('/') ? metadata.filePath : '/' + metadata.filePath;
406
394
  const result = {
407
395
  // 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
408
396
  // 与后续 file ls / file stat 返回的展示名保持一致;缺失时降级用本地 fileName。
@@ -425,7 +413,7 @@ async function uploadFile(opts) {
425
413
  */
426
414
  function sanitizeFileName(fileName) {
427
415
  const illegalChars = /[:"\\/*?<>|,;]/g;
428
- return encodeURIComponent(fileName.replace(illegalChars, "")) || "download_file";
416
+ return encodeURIComponent(fileName.replace(illegalChars, '')) || 'download_file';
429
417
  }
430
418
  // ── 预签下载 URL ──
431
419
  /**
@@ -441,15 +429,15 @@ async function signDownload(opts) {
441
429
  expiresIn: opts.expiresIn,
442
430
  };
443
431
  const body = await (0, client_1.doPost)(url, reqBody, {
444
- errorContext: "sign download URL",
432
+ errorContext: 'sign download URL',
445
433
  });
446
434
  // 后端对不存在文件走业务 ErrorCode(status_code=k_img_ec_000034),
447
435
  // 由 ensureSuccess → BIZ_ERR_MAP 统一抛出 FILE_NOT_FOUND,此处无需兜底
448
436
  const data = extractEnvelope(body);
449
- const signed = data.signedURL ?? "";
437
+ const signed = data.signedURL ?? '';
450
438
  const expiresAt = new Date(Date.now() + opts.expiresIn * 1000).toISOString();
451
- const displayPath = opts.filePath.startsWith("/") ? opts.filePath : `/${opts.filePath}`;
452
- const fileName = displayPath.split("/").pop() ?? displayPath;
439
+ const displayPath = opts.filePath.startsWith('/') ? opts.filePath : `/${opts.filePath}`;
440
+ const fileName = displayPath.split('/').pop() ?? displayPath;
453
441
  return {
454
442
  file_name: fileName,
455
443
  path: displayPath,
@@ -468,11 +456,11 @@ async function downloadFile(opts) {
468
456
  }
469
457
  catch (err) {
470
458
  (0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
471
- throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
459
+ throw new error_1.AppError('FILE_DOWNLOAD_FAILED', `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
472
460
  }
473
461
  (0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
474
462
  if (!res.ok) {
475
- throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
463
+ throw new error_1.AppError('FILE_DOWNLOAD_FAILED', `Download failed with status ${String(res.status)}`);
476
464
  }
477
465
  const array = new Uint8Array(await res.arrayBuffer());
478
466
  const buf = Buffer.from(array);
@@ -492,17 +480,17 @@ async function deleteFiles(opts) {
492
480
  filePaths: opts.filePaths.map(toApiPath),
493
481
  };
494
482
  const body = await (0, client_1.doRequest)({
495
- method: "DELETE",
483
+ method: 'DELETE',
496
484
  url,
497
- headers: { "Content-Type": "application/json" },
485
+ headers: { 'Content-Type': 'application/json' },
498
486
  body: JSON.stringify(reqBody),
499
- }, { errorContext: "delete files" });
487
+ }, { errorContext: 'delete files' });
500
488
  const data = extractEnvelope(body);
501
489
  // 后端响应里只给出被删除成功的 attachments(无前导 /),没删掉的靠对比输入判断
502
- const toDisplay = (p) => (p.startsWith("/") ? p : `/${p}`);
490
+ const toDisplay = (p) => (p.startsWith('/') ? p : `/${p}`);
503
491
  const deletedSet = new Set();
504
492
  for (const att of data.attachments ?? []) {
505
- const p = att.filePath ?? att.name ?? "";
493
+ const p = att.filePath ?? att.name ?? '';
506
494
  if (p)
507
495
  deletedSet.add(toApiPath(p));
508
496
  }
@@ -514,8 +502,22 @@ async function deleteFiles(opts) {
514
502
  deleted.push(toDisplay(normalized));
515
503
  }
516
504
  else {
517
- failed.push({ path: toDisplay(normalized), error: "file not found or not deleted" });
505
+ failed.push({ path: toDisplay(normalized), error: 'file not found or not deleted' });
518
506
  }
519
507
  }
520
508
  return { deleted, failed };
521
509
  }
510
+ // ── storage quota ──
511
+ /**
512
+ * 后端:GET /v1/storage/app/{appId}/bucket/{bucketId}/quota
513
+ * 单 bucket 用量;StorageQuotaBytes 暂未对接,CLI 拿到 0 时按 "—" 渲染。
514
+ * bucketId 缺省时走默认 bucket(与 ls / stat / cp 等一致)。
515
+ */
516
+ async function getStorageQuota(opts) {
517
+ const bucketId = opts.bucketId ?? (await (0, client_1.getDefaultBucketId)(opts.appId));
518
+ const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/quota`;
519
+ const body = await (0, client_1.doGet)(url, {
520
+ errorContext: `fetch storage quota for app '${opts.appId}' bucket '${bucketId}'`,
521
+ });
522
+ return extractEnvelope(body);
523
+ }