@lark-apaas/miaoda-cli 0.1.0-alpha.465bdb8 → 0.1.0-alpha.5f650e8

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.
@@ -24,7 +24,9 @@ async function execSql(opts) {
24
24
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
25
25
  dbBranch: opts.dbBranch,
26
26
  });
27
+ const start = Date.now();
27
28
  const response = await client.post(url, { sql: opts.sql });
29
+ (0, client_1.traceHttp)("POST", url, start, response);
28
30
  if (!response.ok) {
29
31
  // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
30
32
  let body = null;
@@ -57,7 +59,9 @@ async function getSchema(opts) {
57
59
  includeStats: opts.includeStats ? "true" : undefined,
58
60
  dbBranch: opts.dbBranch,
59
61
  });
62
+ const start = Date.now();
60
63
  const response = await client.get(url);
64
+ (0, client_1.traceHttp)("GET", url, start, response);
61
65
  if (!response.ok) {
62
66
  let body = null;
63
67
  try {
@@ -73,28 +77,32 @@ async function getSchema(opts) {
73
77
  const body = (await response.json());
74
78
  return (0, client_1.extractData)(body);
75
79
  }
76
- // ── db data import → InnerImportData ──
80
+ // ── db data import → InnerAdminImportData ──
77
81
  /**
78
82
  * 导入文件。
79
- * 后端:POST /v1/dataloom/app/{appId}/data/import?tableName=...&format=csv|json&dbBranch=main
83
+ * 后端:POST /v1/dataloom/app/{appId}/data/import
80
84
  *
81
- * Body 为原始文件字节(不走 JSON envelope)。
85
+ * 全字段走 JSON body envelope(idl-larkgw 36125a2f):
86
+ * {tableName, format, records, dbBranch?}
87
+ *
88
+ * `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
89
+ * Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
90
+ * 字符串后塞进 envelope 即可。
82
91
  */
83
92
  async function importData(opts) {
84
93
  const client = (0, http_1.getHttpClient)();
85
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
94
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
95
+ const reqBody = {
86
96
  tableName: opts.tableName,
87
97
  format: opts.format,
88
- dbBranch: opts.dbBranch,
89
- });
90
- const contentType = opts.format === "csv" ? "text/csv" : "application/json";
91
- const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
92
- const response = await client.request({
93
- method: "POST",
94
- url,
95
- headers: { "Content-Type": contentType },
96
- body: ab,
97
- });
98
+ records: opts.body.toString("utf8"),
99
+ };
100
+ if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
101
+ reqBody.dbBranch = opts.dbBranch;
102
+ }
103
+ const start = Date.now();
104
+ const response = await client.post(url, reqBody);
105
+ (0, client_1.traceHttp)("POST", url, start, response);
98
106
  if (!response.ok) {
99
107
  let body = null;
100
108
  try {
@@ -107,31 +115,37 @@ async function importData(opts) {
107
115
  (0, client_1.extractData)(body);
108
116
  throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
109
117
  }
110
- // 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
118
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
111
119
  const body = (await response.json());
112
120
  const data = (0, client_1.extractData)(body);
113
121
  return {
114
122
  tableName: data.tableName ?? opts.tableName,
115
- rows: data.rows ?? 0,
123
+ recordCount: data.recordCount ?? 0,
116
124
  durationMs: data.durationMs ?? 0,
117
125
  };
118
126
  }
119
- // ── db data export → InnerExportData ──
127
+ // ── db data export → InnerAdminExportData ──
120
128
  /**
121
129
  * 导出数据。
122
- * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
130
+ * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
123
131
  *
124
- * 响应 body 为原始 CSV/JSON 字节(不走 envelope)。错误仍通过 HTTP 4xx/5xx + BaseResp 传达。
132
+ * 所有参数(含 limit)均按 IDL `api.query` query string;HTTP 方法是 POST(对齐
133
+ * inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
134
+ * 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
135
+ * 回传,错误仍走 HTTP 4xx/5xx + envelope。
125
136
  */
126
137
  async function exportData(opts) {
127
138
  const client = (0, http_1.getHttpClient)();
128
139
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
129
140
  tableName: opts.tableName,
130
141
  format: opts.format,
142
+ limit: String(opts.limit ?? 5000),
131
143
  dbBranch: opts.dbBranch,
132
144
  });
133
- const reqBody = { limit: opts.limit ?? 5000 };
134
- const response = await client.post(url, reqBody);
145
+ // POST + body:所有业务参数都在 query
146
+ const start = Date.now();
147
+ const response = await client.request({ method: "POST", url });
148
+ (0, client_1.traceHttp)("POST", url, start, response);
135
149
  if (!response.ok) {
136
150
  // 错误路径:body 是 JSON envelope
137
151
  let body = null;
@@ -145,7 +159,8 @@ async function exportData(opts) {
145
159
  (0, client_1.extractData)(body);
146
160
  throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
147
161
  }
148
- // 成功路径:响应 body 是原始 CSV/JSON 字节
162
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
163
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
149
164
  const contentType = response.headers.get("Content-Type") ??
150
165
  (opts.format === "csv" ? "text/csv" : "application/json");
151
166
  const ab = await response.arrayBuffer();
@@ -153,10 +168,33 @@ async function exportData(opts) {
153
168
  if (buf.length === 0) {
154
169
  throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
155
170
  }
171
+ // Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
172
+ // InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
173
+ // 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
174
+ if (opts.format === "csv" && /application\/json/i.test(contentType)) {
175
+ try {
176
+ const parsed = JSON.parse(buf.toString("utf8"));
177
+ if (parsed.status_code != null && parsed.status_code !== "0") {
178
+ // 复用 extractData 的错误映射逻辑(throw AppError)
179
+ (0, client_1.extractData)(parsed);
180
+ }
181
+ }
182
+ catch (err) {
183
+ // 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
184
+ // 真的是 CSV 文本,继续按成功流程走
185
+ if (err instanceof error_1.AppError)
186
+ throw err;
187
+ }
188
+ }
189
+ // 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
190
+ const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
191
+ const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
192
+ const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
156
193
  return {
157
194
  tableName: opts.tableName,
158
195
  format: opts.format,
159
196
  contentType,
160
197
  body: buf,
198
+ recordCount,
161
199
  };
162
200
  }
@@ -1,10 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SQLSTATE_MAP = void 0;
4
+ exports.traceHttp = traceHttp;
4
5
  exports.ensureInnerSuccess = ensureInnerSuccess;
5
6
  exports.extractData = extractData;
6
7
  exports.buildInnerUrl = buildInnerUrl;
7
8
  const error_1 = require("../../utils/error");
9
+ const logger_1 = require("../../utils/logger");
10
+ /**
11
+ * 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
12
+ *
13
+ * 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
14
+ * 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
15
+ *
16
+ * 使用约定:
17
+ * const start = Date.now();
18
+ * const response = await client.post(url, body);
19
+ * traceHttp("POST", url, start, response);
20
+ * // 或错误路径:traceHttp("POST", url, start, err.response, err)
21
+ */
22
+ function traceHttp(method, url, start, response, err) {
23
+ // debug() 内部已判断 verbose 开关,这里不重复判断;保持调用点轻量
24
+ try {
25
+ const cost = Date.now() - start;
26
+ const status = response?.status ?? 0;
27
+ const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
28
+ if (err !== undefined) {
29
+ const errMsg = err instanceof Error
30
+ ? err.message
31
+ : typeof err === "string"
32
+ ? err
33
+ : JSON.stringify(err);
34
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
35
+ return;
36
+ }
37
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
38
+ }
39
+ catch {
40
+ // debug 失败不应影响业务,吞掉
41
+ }
42
+ }
8
43
  /**
9
44
  * 校验 dataloom InnerAPI 响应的 envelope。
10
45
  *
@@ -280,6 +280,7 @@ async function uploadFile(opts) {
280
280
  // Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
281
281
  // 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
282
282
  const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
283
+ const uploadStart = Date.now();
283
284
  const res = await fetch(pre.uploadURL, {
284
285
  method: "PUT",
285
286
  headers: {
@@ -288,6 +289,7 @@ async function uploadFile(opts) {
288
289
  },
289
290
  body: ab,
290
291
  });
292
+ (0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
291
293
  if (!res.ok) {
292
294
  throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
293
295
  }
@@ -364,12 +366,15 @@ async function signDownload(opts) {
364
366
  */
365
367
  async function downloadFile(opts) {
366
368
  let res;
369
+ const downloadStart = Date.now();
367
370
  try {
368
371
  res = await fetch(opts.signedURL);
369
372
  }
370
373
  catch (err) {
374
+ (0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
371
375
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
372
376
  }
377
+ (0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
373
378
  if (!res.ok) {
374
379
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
375
380
  }
@@ -8,7 +8,34 @@ exports.doPost = doPost;
8
8
  exports.doRequest = doRequest;
9
9
  const http_1 = require("../../utils/http");
10
10
  const error_1 = require("../../utils/error");
11
+ const logger_1 = require("../../utils/logger");
11
12
  const http_client_1 = require("@lark-apaas/http-client");
13
+ /**
14
+ * 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
15
+ *
16
+ * 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
17
+ * 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
18
+ */
19
+ function traceHttp(method, url, start, response, err) {
20
+ try {
21
+ const cost = Date.now() - start;
22
+ const status = response?.status ?? 0;
23
+ const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
24
+ if (err !== undefined) {
25
+ const errMsg = err instanceof Error
26
+ ? err.message
27
+ : typeof err === "string"
28
+ ? err
29
+ : JSON.stringify(err);
30
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
31
+ return;
32
+ }
33
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
34
+ }
35
+ catch {
36
+ // debug 失败不应影响业务,吞掉
37
+ }
38
+ }
12
39
  /** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
13
40
  const bucketCache = new Map();
14
41
  /**
@@ -130,33 +157,42 @@ async function mapHttpError(err, opts) {
130
157
  * 通过第三个参数显式传入 getRuntimeHttpClient() 切换。
131
158
  */
132
159
  async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
160
+ const start = Date.now();
133
161
  try {
134
162
  const response = await client.get(url);
163
+ traceHttp("GET", url, start, response);
135
164
  return (await response.json());
136
165
  }
137
166
  catch (err) {
167
+ traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
138
168
  await mapHttpError(err, opts);
139
169
  throw err; // 不可达,mapHttpError 必定 throw
140
170
  }
141
171
  }
142
172
  /** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
143
173
  async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
174
+ const start = Date.now();
144
175
  try {
145
176
  const response = await client.post(url, body);
177
+ traceHttp("POST", url, start, response);
146
178
  return (await response.json());
147
179
  }
148
180
  catch (err) {
181
+ traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
149
182
  await mapHttpError(err, opts);
150
183
  throw err;
151
184
  }
152
185
  }
153
186
  /** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
154
187
  async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
188
+ const start = Date.now();
155
189
  try {
156
190
  const response = await client.request(cfg);
191
+ traceHttp(cfg.method, cfg.url, start, response);
157
192
  return (await response.json());
158
193
  }
159
194
  catch (err) {
195
+ traceHttp(cfg.method, cfg.url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
160
196
  await mapHttpError(err, opts);
161
197
  throw err;
162
198
  }
@@ -2,74 +2,177 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerDbCommands = registerDbCommands;
4
4
  const index_1 = require("../../../cli/handlers/db/index");
5
- const shared_1 = require("../../../cli/commands/shared");
6
5
  function registerDbCommands(program) {
7
6
  const dbCmd = program
8
7
  .command("db")
9
- .description("数据操作:SQL 执行、表结构查询、数据导入导出");
8
+ .description("数据库操作:执行 SQL、查看表结构、导入导出数据")
9
+ .usage("<command> [flags]");
10
10
  dbCmd.action(() => {
11
11
  dbCmd.outputHelp();
12
12
  });
13
+ dbCmd.addHelpText("after", `
14
+ Examples:
15
+ $ miaoda db sql "SELECT * FROM users LIMIT 5"
16
+ $ miaoda db schema list
17
+ $ miaoda db schema get users
18
+ $ miaoda db data import users.csv
19
+ $ miaoda db data export users
20
+ `);
13
21
  dbCmd
14
22
  .command("sql")
15
- .description("执行任意 SQL(SELECT / DML / DDL / TCL / EXPLAIN);query 省略时读 stdin")
16
- .argument("[query]", "要执行的 SQL 语句;省略时从 stdin 读取")
17
- .addOption((0, shared_1.appIdOption)())
18
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
23
+ .description("执行 SQL 语句(DDL/DML/SELECT 任意 PG 语句)")
24
+ .usage("[query] [flags]")
25
+ .argument("[query]", "SQL 语句;省略时从 stdin 读取")
26
+ .option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
19
27
  .action(async (query, opts) => {
20
28
  await (0, index_1.handleDbSql)(query, opts);
21
- });
29
+ })
30
+ .addHelpText("after", `
31
+ Notes:
32
+ - DML 单语句 affectedRows 与 SELECT 单语句返回行数上限均为 1000,超过即拒绝
33
+ - 多语句以 ; 分隔,失败时错误响应携带 statement_index 定位失败位置
34
+ - 支持用户自管事务 BEGIN / COMMIT / ROLLBACK;事务中途失败服务端会自动 ROLLBACK
35
+
36
+ Examples:
37
+ $ miaoda db sql "SELECT * FROM users LIMIT 3"
38
+ ✓ 3 rows
39
+
40
+ $ miaoda db sql "CREATE TABLE t(id int)"
41
+ ✓ Statement executed
42
+
43
+ $ cat migration.sql | miaoda db sql
44
+ ✓ 5 statements executed
45
+
46
+ $ miaoda db sql "SELECT count(*) FROM orders" --json
47
+ [{"count": 1234}]
48
+
49
+ # 报错:多语句中第 1 条失败
50
+ $ miaoda db sql "CREATE TABLE t(id int); SELECT * FROM no_such"
51
+ Error: TABLE_NOT_FOUND at statement 1
52
+ hint: Run \`miaoda db schema list\` to see existing tables.
53
+ `);
22
54
  // schema 二级资源分组
23
55
  const schemaCmd = dbCmd
24
56
  .command("schema")
25
- .description("表结构查询");
57
+ .description("查看表结构(list / get)")
58
+ .usage("<command> [flags]");
26
59
  schemaCmd.action(() => {
27
60
  schemaCmd.outputHelp();
28
61
  });
29
62
  schemaCmd
30
63
  .command("list")
31
- .description("列出应用所有表(rows / size / columns / updated_at)")
32
- .addOption((0, shared_1.appIdOption)())
33
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
64
+ .description("列出当前应用的所有表(含行数估算、占用大小、列数)")
65
+ .usage("[flags]")
66
+ .option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
34
67
  .action(async (opts) => {
35
68
  await (0, index_1.handleDbSchemaList)(opts);
36
- });
69
+ })
70
+ .addHelpText("after", `
71
+ Examples:
72
+ $ miaoda db schema list
73
+ name rows size_bytes columns
74
+ users 120 65536 6
75
+ orders 5400 327680 9
76
+
77
+ $ miaoda db schema list --json | jq '.[].name'
78
+ "users"
79
+ "orders"
80
+
81
+ $ miaoda db schema list --env main
82
+ `);
37
83
  schemaCmd
38
84
  .command("get")
39
- .description("查看单表结构(字段 / 索引 / DDL)")
40
- .argument("<table>", "表名")
41
- .addOption((0, shared_1.appIdOption)())
42
- .option("--ddl", "TTY 下强制输出完整 DDL 而非结构化概览")
43
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
85
+ .description("查看单张表的字段、索引与建表语句;判断表是否存在也用这条")
86
+ .usage("<table> [flags]")
87
+ .argument("<table>", "表名(不带 schema 前缀)")
88
+ .option("--ddl", "只输出 CREATE TABLE 语句,便于复制 / 备份")
89
+ .option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
44
90
  .action(async (table, opts) => {
45
91
  await (0, index_1.handleDbSchemaGet)(table, opts);
46
- });
92
+ })
93
+ .addHelpText("after", `
94
+ Notes:
95
+ - 用作"判断表是否存在"探针时:退出码 0 → 表存在;错误码 TABLE_NOT_FOUND → 表不存在
96
+
97
+ Examples:
98
+ $ miaoda db schema get users
99
+ CREATE TABLE users (
100
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
101
+ ...
102
+ );
103
+
104
+ $ miaoda db schema get users --ddl
105
+ CREATE TABLE users ( ... );
106
+
107
+ $ miaoda db schema get users --json
108
+ {"name":"users","columns":[...],"indexes":[...]}
109
+
110
+ # 报错:表不存在
111
+ $ miaoda db schema get no_such
112
+ Error: TABLE_NOT_FOUND
113
+ hint: Run \`miaoda db schema list\` to see all tables.
114
+ `);
47
115
  // data 二级资源分组
48
116
  const dataCmd = dbCmd
49
117
  .command("data")
50
- .description("数据导入 / 导出");
118
+ .description("批量导入与导出数据(import / export)")
119
+ .usage("<command> [flags]");
51
120
  dataCmd.action(() => {
52
121
  dataCmd.outputHelp();
53
122
  });
54
123
  dataCmd
55
124
  .command("import")
56
- .description(" CSV / JSON 文件导入数据(P0:≤ 1 MB / 5000 行,单事务原子)")
57
- .argument("<file>", "本地文件路径")
58
- .addOption((0, shared_1.appIdOption)())
59
- .option("--table <name>", "目标表名;省略时按文件名推断")
60
- .option("--format <fmt>", "csv | json;省略时按文件扩展名推断")
125
+ .description("把本地 CSV / JSON 文件导入到表(单次最多 5000 / 1 MB,全部成功或全部回滚)")
126
+ .usage("<file> [flags]")
127
+ .argument("<file>", "本地文件路径(CSV 或 JSON)")
128
+ .option("--table <name>", "目标表名;缺省按文件名(去扩展名)推断")
129
+ .option("--format <fmt>", "文件格式 csv / json;缺省按文件扩展名推断")
61
130
  .action(async (file, opts) => {
62
131
  await (0, index_1.handleDbDataImport)(file, opts);
63
- });
132
+ })
133
+ .addHelpText("after", `
134
+ Notes:
135
+ - 单次最多 5000 行 / 1 MB;超过请拆批
136
+ - 任一行失败 → 整批回滚,错误响应给出失败行号 + 原因
137
+
138
+ Examples:
139
+ $ miaoda db data import users.csv
140
+ ✓ Imported 120 rows into 'users'
141
+
142
+ $ miaoda db data import data.json --table customers
143
+ ✓ Imported 240 rows into 'customers'
144
+
145
+ $ miaoda db data import dump --table orders --format csv
146
+ ✓ Imported 80 rows into 'orders'
147
+
148
+ # 报错:第 5 行格式错误,整批回滚
149
+ $ miaoda db data import broken.csv
150
+ Error: ROW_PARSE_FAILED at row 5
151
+ hint: Check column count and value types match the table schema.
152
+ `);
64
153
  dataCmd
65
154
  .command("export")
66
- .description("导出整表到 CSV / JSON(P0:≤ 1 MB / 5000 行)")
67
- .argument("<table>", "表名")
68
- .addOption((0, shared_1.appIdOption)())
69
- .option("--format <fmt>", "csv | json;默认 csv")
70
- .option("-f, --file <path>", "输出文件路径;默认 <table>.<format>")
71
- .option("--limit <n>", "最多导出行数(≤ 5000)")
155
+ .description("把整张表导出为 CSV / JSON 文件(单次最多 5000 / 1 MB)")
156
+ .usage("<table> [flags]")
157
+ .argument("<table>", "表名(不带 schema 前缀)")
158
+ .option("--format <fmt>", "导出格式 csv / json,默认 csv")
159
+ .option("-f, --file <path>", "输出文件路径,默认 <table>.<format>")
160
+ .option("--limit <n>", "最多导出行数(不超过 5000)")
72
161
  .action(async (table, opts) => {
73
162
  await (0, index_1.handleDbDataExport)(table, opts);
74
- });
163
+ })
164
+ .addHelpText("after", `
165
+ Examples:
166
+ $ miaoda db data export users
167
+ ✓ Exported 120 rows to ./users.csv
168
+
169
+ $ miaoda db data export users --format json
170
+ ✓ Exported 120 rows to ./users.json
171
+
172
+ $ miaoda db data export users -f /tmp/u.csv
173
+ ✓ Exported 120 rows to /tmp/u.csv
174
+
175
+ $ miaoda db data export users --limit 1000
176
+ ✓ Exported 1000 rows to ./users.csv
177
+ `);
75
178
  }
@@ -2,66 +2,163 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerFileCommands = registerFileCommands;
4
4
  const index_1 = require("../../../cli/handlers/file/index");
5
- const shared_1 = require("../../../cli/commands/shared");
5
+ const error_1 = require("../../../utils/error");
6
+ /**
7
+ * commander option 校验器:把 --limit <n> 解析成正整数(≥1)。
8
+ * 默认值(如 "50")会先经过这里被规范化成 number。
9
+ * 非整数 / 负数 / 0 抛 AppError("ARGS_INVALID"),由 main.ts 的全局 catch
10
+ * 走 emitError,同时 process.exitCode 由 commander 自然为 1。
11
+ */
12
+ function parsePositiveInt(raw) {
13
+ const n = Number(raw);
14
+ if (!Number.isInteger(n) || n < 1) {
15
+ throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
16
+ }
17
+ return n;
18
+ }
6
19
  function registerFileCommands(program) {
7
20
  const fileCmd = program
8
21
  .command("file")
9
- .description("文件操作:上传、下载、删除、查询");
22
+ .description("文件操作:上传、下载、删除、查询")
23
+ .usage("<command> [flags]");
10
24
  fileCmd.action(() => {
11
25
  fileCmd.outputHelp();
12
26
  });
27
+ fileCmd.addHelpText("after", `
28
+ Examples:
29
+ $ miaoda file ls
30
+ $ miaoda file stat report.pdf
31
+ $ miaoda file cp report.pdf /uploads/
32
+ $ miaoda file cp /uploads/report.pdf .
33
+ $ miaoda file sign report.pdf
34
+ $ miaoda file rm a.txt b.txt -y
35
+ `);
13
36
  fileCmd
14
37
  .command("ls")
15
- .description("列出应用下的文件")
16
- .argument("[query]", "可选筛选值:形似 /path、包含 / 或 16+位 fileKey 时按 path 精确匹配;否则按 file_name 精确匹配")
17
- .addOption((0, shared_1.appIdOption)())
18
- .option("--path <path>", "按 path 精确匹配(跳过自动识别)")
19
- .option("--name <name>", "按 file_name 精确匹配(跳过自动识别)")
20
- .option("--type <mime>", "按 MIME 类型过滤(精确匹配)")
21
- .option("--size-gt <size>", "大小大于,支持 B/KB/MB/GB")
22
- .option("--size-lt <size>", "大小小于")
23
- .option("--uploaded-since <time>", "上传时间晚于(ISO 8601)")
24
- .option("--limit <n>", "返回条数上限", "50")
25
- .option("--cursor <token>", "分页游标")
26
- .option("--all", "自动翻页聚合全部结果")
38
+ .description("列出应用下的文件,支持名称 / 路径 / MIME / 大小 / 时间多维筛选")
39
+ .usage("[query] [flags]")
40
+ .argument("[query]", "可选筛选值:以 / 开头视为路径(精确匹配),否则按文件名匹配")
41
+ .option("--path <path>", "按路径精确匹配")
42
+ .option("--name <name>", "按文件名精确匹配")
43
+ .option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
44
+ .option("--size-gt <size>", "文件大小下限(支持 B/KB/MB/GB")
45
+ .option("--size-lt <size>", "文件大小上限(支持 B/KB/MB/GB)")
46
+ .option("--uploaded-since <time>", "上传时间下限(ISO 8601)")
47
+ .option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
48
+ .option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
49
+ .option("--all", "自动翻页返回全部结果")
27
50
  .action(async (query, opts) => {
28
51
  await (0, index_1.handleFileLs)({ ...opts, query });
29
- });
52
+ })
53
+ .addHelpText("after", `
54
+ Examples:
55
+ $ miaoda file ls
56
+ path size uploaded_at
57
+ /uploads/report.pdf 1.2 MB 2026-04-20 10:00
58
+ /uploads/photo.png 800 KB 2026-04-21 14:30
59
+
60
+ $ miaoda file ls report.pdf
61
+ $ miaoda file ls /uploads/report.pdf
62
+ $ miaoda file ls --type image/png --size-gt 1MB
63
+ $ miaoda file ls --uploaded-since 2026-04-01
64
+
65
+ $ miaoda file ls --all --json | jq '.[].name'
66
+ "report.pdf"
67
+ "photo.png"
68
+ `);
30
69
  fileCmd
31
70
  .command("stat")
32
- .description("查看指定文件的元数据")
33
- .argument("<file>", "path 或 file_name:自动识别(见 ls 命令说明)")
34
- .addOption((0, shared_1.appIdOption)())
71
+ .description("查看文件元数据(不含下载链接,需要链接用 sign)")
72
+ .usage("<file> [flags]")
73
+ .argument("<file>", "文件的路径或文件名(自动识别)")
35
74
  .action(async (file, opts) => {
36
75
  await (0, index_1.handleFileStat)(file, opts);
37
- });
76
+ })
77
+ .addHelpText("after", `
78
+ Examples:
79
+ $ miaoda file stat report.pdf
80
+ path: /uploads/report.pdf
81
+ size: 1.2 MB
82
+ type: application/pdf
83
+ uploaded_at: 2026-04-20 10:00
84
+
85
+ $ miaoda file stat /uploads/report.pdf
86
+ $ miaoda file stat report.pdf --json
87
+ {"path":"/uploads/report.pdf","size":1258291,...}
88
+
89
+ # 报错:文件不存在
90
+ $ miaoda file stat no_such.pdf
91
+ Error: FILE_NOT_FOUND
92
+ hint: Run \`miaoda file ls\` to see available files.
93
+ `);
38
94
  fileCmd
39
95
  .command("cp")
40
- .description("上传或下载文件(方向由 src/dst 自动判断)")
41
- .argument("<src>", "源:本地路径 或 远程 /path / file_name")
96
+ .description("上传或下载文件,按 src/dst 自动判断方向")
97
+ .usage("<src> <dst> [flags]")
98
+ .argument("<src>", "源:本地文件路径 或 远程文件路径/名")
42
99
  .argument("<dst>", "目标:本地路径 或 远程路径")
43
- .addOption((0, shared_1.appIdOption)())
44
- .option("--rename <name>", "上传后使用的新文件名")
100
+ .option("--rename <name>", "上传后在远端使用的新文件名")
45
101
  .action(async (src, dst, opts) => {
46
102
  await (0, index_1.handleFileCp)(src, dst, opts);
47
- });
103
+ })
104
+ .addHelpText("after", `
105
+ Notes:
106
+ - src 是本地存在的文件 → 上传
107
+ - src 不是本地文件而 dst 看起来是本地路径 → 下载
108
+
109
+ Examples:
110
+ $ miaoda file cp ./report.pdf /uploads/
111
+ ✓ Uploaded report.pdf → /uploads/report.pdf
112
+
113
+ $ miaoda file cp ./report.pdf /uploads/ --rename r.pdf
114
+ ✓ Uploaded report.pdf → /uploads/r.pdf
115
+
116
+ $ miaoda file cp /uploads/report.pdf .
117
+ ✓ Downloaded /uploads/report.pdf → ./report.pdf
118
+
119
+ $ miaoda file cp /uploads/report.pdf ./local.pdf
120
+ ✓ Downloaded /uploads/report.pdf → ./local.pdf
121
+ `);
48
122
  fileCmd
49
123
  .command("rm")
50
- .description("删除一个或多个文件(best-effort)")
51
- .argument("[paths...]", "path 或 file_name:形似 /path、含 / 或 16+位 fileKey 时按 path 精确删除,其余按 file_name 查找后删除")
52
- .addOption((0, shared_1.appIdOption)())
53
- .option("-n, --name <name>", "强制按 file_name 查找后删除(跳过自动识别);可重复指定;多匹配报 AMBIGUOUS_FILE_NAME", (value, prev) => [...(prev ?? []), value])
54
- .option("-y, --yes", "跳过交互确认")
124
+ .description("批量删除文件(单次最多 100 个,部分失败不影响其他)")
125
+ .usage("[paths...] [flags]")
126
+ .argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
127
+ .option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
128
+ .option("-y, --yes", "跳过交互确认(脚本 / agent 调用必加)")
55
129
  .action(async (paths, opts) => {
56
130
  await (0, index_1.handleFileRm)(paths, opts);
57
- });
131
+ })
132
+ .addHelpText("after", `
133
+ Notes:
134
+ - 单次最多 100 个;超过请拆批
135
+ - 部分失败响应里会列出每条 success / failed 状态,整批仍 exit 0
136
+
137
+ Examples:
138
+ $ miaoda file rm /uploads/a.pdf /uploads/b.pdf -y
139
+ ✓ Deleted 2 files
140
+
141
+ $ miaoda file rm a.pdf b.pdf -y
142
+ ✓ Deleted 2 files
143
+
144
+ $ miaoda file rm -n a.pdf -n b.pdf -y
145
+ ✓ Deleted 2 files
146
+ `);
58
147
  fileCmd
59
148
  .command("sign")
60
- .description("生成文件临时下载链接")
61
- .argument("<file>", "path 或 file_name:自动识别(见 ls 命令说明)")
62
- .addOption((0, shared_1.appIdOption)())
63
- .option("--expires <duration>", "过期时长(默认 7d,最长 30d)")
149
+ .description("生成可分享的临时下载链接")
150
+ .usage("<file> [flags]")
151
+ .argument("<file>", "文件的路径或文件名")
152
+ .option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 单位(默认 7d,最长 30d)")
64
153
  .action(async (file, opts) => {
65
154
  await (0, index_1.handleFileSign)(file, opts);
66
- });
155
+ })
156
+ .addHelpText("after", `
157
+ Examples:
158
+ $ miaoda file sign report.pdf
159
+ https://...?expires=... (valid 7d)
160
+
161
+ $ miaoda file sign report.pdf --expires 30m
162
+ $ miaoda file sign report.pdf --expires 24h
163
+ `);
67
164
  }
@@ -5,7 +5,8 @@ const index_1 = require("../../../cli/handlers/plugin/index");
5
5
  function registerPluginCommands(program) {
6
6
  const pluginCmd = program
7
7
  .command("plugin")
8
- .description("插件管理:安装/更新/移除插件包,查询 capability 实例");
8
+ .description("插件管理:安装/更新/移除插件包,查询 capability 实例")
9
+ .usage("<command> [flags]");
9
10
  pluginCmd.action(() => {
10
11
  pluginCmd.outputHelp();
11
12
  });
@@ -1,18 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appIdOption = appIdOption;
4
3
  exports.softRequiredOption = softRequiredOption;
5
4
  exports.resolveAppId = resolveAppId;
6
5
  exports.withHelp = withHelp;
7
6
  exports.failArgs = failArgs;
8
7
  const commander_1 = require("commander");
9
8
  const error_1 = require("../../utils/error");
10
- /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption())。
11
- * Commander 的 .env() 只接受单个变量名,第二个兜底 `app_id` 在 resolveAppId 里手动检查。
12
- */
13
- function appIdOption() {
14
- return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
15
- }
16
9
  /**
17
10
  * soft-required: Commander 类型上 optional,runtime 校验必填。
18
11
  */
@@ -20,15 +13,16 @@ function softRequiredOption(name, desc) {
20
13
  return new commander_1.Option(name, desc);
21
14
  }
22
15
  /**
23
- * 解析 appId,优先级:CLI flag > MIAODA_APP_ID > app_id > 抛错。
24
- * app_id 是部分外部沙箱环境注入应用 ID 的别名,作为兜底兼容(小写下划线形态)。
16
+ * 解析 appId,从环境变量 MIAODA_APP_ID app_id(小写下划线,部分外部沙箱注入)读取。
17
+ * 缺失时抛 APP_ID_MISSING,由全局 catch 处理。
18
+ *
19
+ * opts 里仍保留 appId(可选),用于测试 / 高级场景显式注入;正常 CLI 不暴露此 flag。
25
20
  */
26
21
  function resolveAppId(opts) {
27
22
  const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
28
23
  if (!id) {
29
24
  throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
30
25
  next_actions: [
31
- "传入 --app-id <id>",
32
26
  "设置 export MIAODA_APP_ID=<id>",
33
27
  ],
34
28
  });
@@ -82,15 +82,15 @@ async function handleDbDataImport(file, opts) {
82
82
  data: {
83
83
  file,
84
84
  table: result.tableName,
85
- rows: result.rows,
85
+ rows: result.recordCount,
86
86
  },
87
87
  });
88
88
  return;
89
89
  }
90
90
  const tty = (0, render_1.isStdoutTty)();
91
91
  (0, output_1.emit)(tty
92
- ? `✓ Imported ${file} → table '${result.tableName}' (${String(result.rows)} rows)`
93
- : `OK Imported ${file} -> table '${result.tableName}' (${String(result.rows)} rows)`);
92
+ ? `✓ Imported ${file} → table '${result.tableName}' (${String(result.recordCount)} rows)`
93
+ : `OK Imported ${file} -> table '${result.tableName}' (${String(result.recordCount)} rows)`);
94
94
  }
95
95
  async function handleDbDataExport(table, opts) {
96
96
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -110,7 +110,8 @@ async function handleDbDataExport(table, opts) {
110
110
  throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, { next_actions: [`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`] });
111
111
  }
112
112
  await fs.writeFile(outputPath, result.body);
113
- const rows = countRows(result.body, format);
113
+ // 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
114
+ const rows = result.recordCount ?? countRows(result.body, format);
114
115
  if ((0, output_1.isJsonMode)()) {
115
116
  (0, output_1.emit)({
116
117
  data: {
@@ -37,6 +37,7 @@ exports.handleDbSql = handleDbSql;
37
37
  const api = __importStar(require("../../../api/index"));
38
38
  const error_1 = require("../../../utils/error");
39
39
  const output_1 = require("../../../utils/output");
40
+ const config_1 = require("../../../utils/config");
40
41
  const shared_1 = require("../../../cli/commands/shared");
41
42
  const render_1 = require("../../../utils/render");
42
43
  const index_1 = require("../../../api/db/index");
@@ -125,8 +126,8 @@ function renderSingle(raw) {
125
126
  }
126
127
  function toJson(parsed) {
127
128
  if (parsed.kind === "select") {
128
- // PRD 单 SELECT:data 直接是行数组
129
- return { data: parsed.rows };
129
+ // PRD 单 SELECT:data 直接是行数组(按 --json 字段投影裁剪)
130
+ return { data: projectRows(parsed.rows) };
130
131
  }
131
132
  if (parsed.kind === "dml") {
132
133
  // PRD 单 DML:data = {command, rows_affected}
@@ -147,7 +148,7 @@ function toJson(parsed) {
147
148
  */
148
149
  function toMultiElement(parsed) {
149
150
  if (parsed.kind === "select") {
150
- return { command: "SELECT", rows: parsed.rows };
151
+ return { command: "SELECT", rows: projectRows(parsed.rows) };
151
152
  }
152
153
  if (parsed.kind === "dml") {
153
154
  return { command: parsed.sqlType, rows_affected: parsed.affectedRows };
@@ -155,6 +156,30 @@ function toMultiElement(parsed) {
155
156
  // DDL:用后端给的细粒度 command
156
157
  return { command: parsed.sqlType };
157
158
  }
159
+ /**
160
+ * PRD:`--json id,name` 字段投影。--json 不带值(boolean true)等价于不裁剪。
161
+ * 字段不存在时按 undefined 处理(JSON.stringify 会忽略 undefined value 的 key),
162
+ * 这样 Agent 拿到的 row 永远只含请求过的列。
163
+ */
164
+ function projectRows(rows) {
165
+ const fields = parseJsonFields();
166
+ if (!fields)
167
+ return rows;
168
+ return rows.map((r) => {
169
+ const out = {};
170
+ for (const f of fields) {
171
+ out[f] = r[f];
172
+ }
173
+ return out;
174
+ });
175
+ }
176
+ /** 读取 --json [fields]:返回字段列表;boolean true 或 undefined 返回 null(不裁剪)。 */
177
+ function parseJsonFields() {
178
+ const v = (0, config_1.getConfig)().json;
179
+ if (typeof v !== "string" || v === "")
180
+ return null;
181
+ return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
182
+ }
158
183
  /** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
159
184
  function renderMultiPretty(results) {
160
185
  const tty = (0, render_1.isStdoutTty)();
@@ -66,7 +66,8 @@ function resolveQueryRouting(opts) {
66
66
  }
67
67
  async function handleFileLs(opts) {
68
68
  const appId = (0, shared_1.resolveAppId)(opts);
69
- const limit = opts.limit ? Number(opts.limit) : undefined;
69
+ // commander 已经把 --limit 解析为 number;保留 ?? undefined 兼容老调用
70
+ const limit = opts.limit;
70
71
  const sizeGt = opts.sizeGt ? (0, render_1.parseSize)(opts.sizeGt) : undefined;
71
72
  const sizeLt = opts.sizeLt ? (0, render_1.parseSize)(opts.sizeLt) : undefined;
72
73
  const { path, name } = resolveQueryRouting(opts);
@@ -44,7 +44,7 @@ const shared_1 = require("../../../cli/commands/shared");
44
44
  const index_1 = require("../../../api/file/index");
45
45
  const render_1 = require("../../../utils/render");
46
46
  const node_readline_1 = __importDefault(require("node:readline"));
47
- const MAX_BATCH = 1000;
47
+ const MAX_BATCH = 100;
48
48
  /**
49
49
  * 解析位置参数(自动识别 path / file_name)与 `--name`(强制 file_name)两类输入。
50
50
  *
@@ -130,7 +130,7 @@ async function handleFileRm(paths, opts) {
130
130
  throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
131
131
  }
132
132
  if (totalCount > MAX_BATCH) {
133
- throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 1000 limit`);
133
+ throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
134
134
  }
135
135
  const appId = (0, shared_1.resolveAppId)(opts);
136
136
  // destructive guardrail
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MiaodaHelp = void 0;
4
+ const commander_1 = require("commander");
5
+ /**
6
+ * MiaodaHelp 重写 commander 默认的 --help 输出,使之对齐 CLI 文档规范:
7
+ *
8
+ * 1. 描述放在最前(commander 默认是 Usage 在前、描述在后)
9
+ * 2. "Options:" 重命名为 "Flags:","Global Options:" 重命名为 "Global Flags:"
10
+ * 3. Usage 段独占一行 Heading + 缩进展示 usage 行
11
+ * 4. 段落顺序:描述 → Usage → Arguments → Flags → Global Flags → Commands → addHelpText('after')
12
+ *
13
+ * Notes / Examples 段由各命令通过 addHelpText('after', ...) 自行追加,
14
+ * 本类不直接生成 —— 框架与文案分层。
15
+ */
16
+ class MiaodaHelp extends commander_1.Help {
17
+ // 全局默认开启:所有子命令 --help 都展示 Global Flags 段
18
+ showGlobalOptions = true;
19
+ /**
20
+ * 父级 --help 的 Commands 列表里展示子命令调用形态。规范要求 "<args> [flags]"
21
+ * 顺序,但 commander 默认 subcommandTerm 是 "name [options] <args>"。
22
+ *
23
+ * - 子命令是分组(含下级 subcommand)→ 只显示 name,不带 args/flags
24
+ * - 子命令是 leaf 且配置了 usage() → "name <usage>",对齐 args 在前 / flags 在后
25
+ * - leaf 没配置 usage → 退回 commander 默认行为
26
+ */
27
+ subcommandTerm(cmd) {
28
+ if (cmd.commands.length > 0) {
29
+ return cmd.name();
30
+ }
31
+ const usage = cmd.usage();
32
+ if (usage) {
33
+ return `${cmd.name()} ${usage}`.trim();
34
+ }
35
+ return super.subcommandTerm(cmd);
36
+ }
37
+ formatHelp(cmd, helper) {
38
+ const termWidth = helper.padWidth(cmd, helper);
39
+ const helpWidth = helper.helpWidth ?? 80;
40
+ const formatItem = (term, description) => {
41
+ if (description) {
42
+ const padding = " ".repeat(Math.max(termWidth - term.length, 0) + 2);
43
+ return `${term}${padding}${description}`;
44
+ }
45
+ return term;
46
+ };
47
+ const formatList = (lines) => lines.map((l) => " " + l).join("\n");
48
+ void helpWidth; // 保留以备后续按宽度自动 wrap,当前直接透传 description
49
+ const out = [];
50
+ // 1. 描述
51
+ const desc = helper.commandDescription(cmd);
52
+ if (desc) {
53
+ out.push(desc, "");
54
+ }
55
+ // 2. Usage:独立 heading + 缩进
56
+ out.push("Usage:", ` ${helper.commandUsage(cmd)}`, "");
57
+ // 3. Arguments
58
+ const args = helper.visibleArguments(cmd).map((a) => formatItem(helper.argumentTerm(a), helper.argumentDescription(a)));
59
+ if (args.length) {
60
+ out.push("Arguments:", formatList(args), "");
61
+ }
62
+ // 4. Flags(原 Options)
63
+ const opts = helper.visibleOptions(cmd).map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
64
+ if (opts.length) {
65
+ out.push("Flags:", formatList(opts), "");
66
+ }
67
+ // 5. Global Flags(原 Global Options,showGlobalOptions=true 时启用)
68
+ if (this.showGlobalOptions) {
69
+ const globals = helper.visibleGlobalOptions(cmd).map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
70
+ if (globals.length) {
71
+ out.push("Global Flags:", formatList(globals), "");
72
+ }
73
+ }
74
+ // 6. Commands(仅父级命令组有)
75
+ const subs = helper.visibleCommands(cmd).map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
76
+ if (subs.length) {
77
+ out.push("Commands:", formatList(subs), "");
78
+ }
79
+ // 保留末尾换行:commander 用 join('\n') 拼 addHelpText('after') 段,
80
+ // 这里多留一个 \n,让 Notes / Examples 段与 Flags / Global Flags 段之间空一行。
81
+ return out.join("\n").replace(/\n+$/, "\n");
82
+ }
83
+ }
84
+ exports.MiaodaHelp = MiaodaHelp;
package/dist/main.js CHANGED
@@ -5,15 +5,23 @@ 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 help_1 = require("./cli/help");
8
9
  const config_1 = require("./utils/config");
9
10
  const log_id_1 = require("./utils/log_id");
10
11
  const output_1 = require("./utils/output");
11
12
  const package_json_1 = __importDefault(require("../package.json"));
13
+ // MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
14
+ // 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
15
+ // 子命令)统一走 MiaodaHelp 渲染,避免在每个子命令上重复 configureHelp。
16
+ commander_1.Command.prototype.createHelp = function () {
17
+ return Object.assign(new help_1.MiaodaHelp(), this.configureHelp());
18
+ };
12
19
  const program = new commander_1.Command();
13
20
  const { version } = package_json_1.default;
14
21
  program
15
22
  .name("miaoda")
16
23
  .description("妙搭平台命令行工具")
24
+ .usage("<command> [flags]")
17
25
  .version(version, "-v, --version", "显示版本号")
18
26
  .option("--json [fields]", "JSON 输出,可选字段级选择")
19
27
  .option("--output <format>", "输出格式(pretty|json)", "pretty")
@@ -12,7 +12,7 @@ let runtimeClient;
12
12
  /**
13
13
  * 获取单例 HttpClient(默认:管理端 innerapi)。
14
14
  *
15
- * 管理端链路仅适用于妙搭开发态——baseURL 从 `MIAODA_DEV_INNER_DOMAIN` 读取,
15
+ * 管理端链路仅适用于妙搭开发态——baseURL 从 `MIAODA_DEV_INNER_DOMAIN_WITH_PREFIX` 读取,
16
16
  * 每次请求自动从 `MIAODA_AUTHN_CODE` 读取用户凭证并注入 `X-Miaoda-Client-Token`。
17
17
  * AK/SK 的 `Authorization` / `x-api-key` 照旧叠加。
18
18
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.0-alpha.465bdb8",
3
+ "version": "0.1.0-alpha.5f650e8",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "node": ">=20"
26
26
  },
27
27
  "dependencies": {
28
- "@lark-apaas/http-client": "^0.1.4",
28
+ "@lark-apaas/http-client": "^0.1.5",
29
29
  "commander": "^13.1.0"
30
30
  },
31
31
  "devDependencies": {