@lark-apaas/miaoda-cli 0.1.0-alpha.ec1a658 → 0.1.0-alpha.fccd72b

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.
@@ -6,7 +6,36 @@ exports.importData = importData;
6
6
  exports.exportData = exportData;
7
7
  const http_1 = require("../../utils/http");
8
8
  const error_1 = require("../../utils/error");
9
+ const http_client_1 = require("@lark-apaas/http-client");
9
10
  const client_1 = require("./client");
11
+ /**
12
+ * 把 SDK 抛出的 HttpError 统一映射成 CLI 层错误:
13
+ * 1. 先尝试从 response body 解 envelope,命中 dataloom 业务 code → AppError
14
+ * 2. 兜底返 HttpError,保留真实 status 码与上下文
15
+ *
16
+ * 配合调用点的 try/catch + traceHttp,让 --verbose 在错误路径上也能拿到
17
+ * x-tt-logid 与 status,方便定位线上问题。
18
+ */
19
+ async function mapDbHttpError(err, url, ctx) {
20
+ if (err instanceof error_1.AppError)
21
+ throw err;
22
+ if (err instanceof http_client_1.HttpError) {
23
+ const status = err.response?.status ?? 0;
24
+ const statusText = err.response?.statusText ?? "";
25
+ try {
26
+ const body = (await err.response?.json());
27
+ if (body)
28
+ (0, client_1.extractData)(body); // 业务 code 命中 → 抛 AppError;不命中走兜底
29
+ }
30
+ catch (innerErr) {
31
+ if (innerErr instanceof error_1.AppError)
32
+ throw innerErr;
33
+ // body 解析失败 → 当成无 envelope 的纯 HTTP 错误处理
34
+ }
35
+ throw new error_1.HttpError(status, url, `${ctx}: ${String(status)} ${statusText}`.trim());
36
+ }
37
+ throw err;
38
+ }
10
39
  // CLI 不再为 dbBranch 设默认值:
11
40
  // 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
12
41
  // 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
@@ -25,20 +54,15 @@ async function execSql(opts) {
25
54
  dbBranch: opts.dbBranch,
26
55
  });
27
56
  const start = Date.now();
28
- const response = await client.post(url, { sql: opts.sql });
29
- (0, client_1.traceHttp)("POST", url, start, response);
30
- if (!response.ok) {
31
- // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
32
- let body = null;
33
- try {
34
- body = (await response.json());
35
- }
36
- catch {
37
- // ignore
38
- }
39
- if (body)
40
- (0, client_1.extractData)(body);
41
- throw new error_1.HttpError(response.status, url, `Failed to execute SQL: ${String(response.status)} ${response.statusText}`);
57
+ let response;
58
+ try {
59
+ response = await client.post(url, { sql: opts.sql });
60
+ (0, client_1.traceHttp)("POST", url, start, response);
61
+ }
62
+ catch (err) {
63
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
64
+ await mapDbHttpError(err, url, "Failed to execute SQL");
65
+ throw err; // 不可达
42
66
  }
43
67
  const body = (await response.json());
44
68
  const data = (0, client_1.extractData)(body);
@@ -60,19 +84,15 @@ async function getSchema(opts) {
60
84
  dbBranch: opts.dbBranch,
61
85
  });
62
86
  const start = Date.now();
63
- const response = await client.get(url);
64
- (0, client_1.traceHttp)("GET", url, start, response);
65
- if (!response.ok) {
66
- let body = null;
67
- try {
68
- body = (await response.json());
69
- }
70
- catch {
71
- // ignore
72
- }
73
- if (body)
74
- (0, client_1.extractData)(body);
75
- throw new error_1.HttpError(response.status, url, `Failed to get schema: ${String(response.status)} ${response.statusText}`);
87
+ let response;
88
+ try {
89
+ response = await client.get(url);
90
+ (0, client_1.traceHttp)("GET", url, start, response);
91
+ }
92
+ catch (err) {
93
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
94
+ await mapDbHttpError(err, url, "Failed to get schema");
95
+ throw err; // 不可达
76
96
  }
77
97
  const body = (await response.json());
78
98
  return (0, client_1.extractData)(body);
@@ -101,19 +121,15 @@ async function importData(opts) {
101
121
  reqBody.dbBranch = opts.dbBranch;
102
122
  }
103
123
  const start = Date.now();
104
- const response = await client.post(url, reqBody);
105
- (0, client_1.traceHttp)("POST", url, start, response);
106
- if (!response.ok) {
107
- let body = null;
108
- try {
109
- body = (await response.json());
110
- }
111
- catch {
112
- // ignore
113
- }
114
- if (body)
115
- (0, client_1.extractData)(body);
116
- throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
124
+ let response;
125
+ try {
126
+ response = await client.post(url, reqBody);
127
+ (0, client_1.traceHttp)("POST", url, start, response);
128
+ }
129
+ catch (err) {
130
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
131
+ await mapDbHttpError(err, url, "Failed to import data");
132
+ throw err; // 不可达
117
133
  }
118
134
  // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
119
135
  const body = (await response.json());
@@ -144,20 +160,15 @@ async function exportData(opts) {
144
160
  });
145
161
  // POST + 空 body:所有业务参数都在 query 里
146
162
  const start = Date.now();
147
- const response = await client.request({ method: "POST", url });
148
- (0, client_1.traceHttp)("POST", url, start, response);
149
- if (!response.ok) {
150
- // 错误路径:body JSON envelope
151
- let body = null;
152
- try {
153
- body = (await response.json());
154
- }
155
- catch {
156
- // ignore
157
- }
158
- if (body)
159
- (0, client_1.extractData)(body);
160
- throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
163
+ let response;
164
+ try {
165
+ response = await client.request({ method: "POST", url });
166
+ (0, client_1.traceHttp)("POST", url, start, response);
167
+ }
168
+ catch (err) {
169
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
170
+ await mapDbHttpError(err, url, "Failed to export data");
171
+ throw err; // 不可达
161
172
  }
162
173
  // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
163
174
  // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
@@ -5,65 +5,197 @@ const index_1 = require("../../../cli/handlers/db/index");
5
5
  function registerDbCommands(program) {
6
6
  const dbCmd = program
7
7
  .command("db")
8
- .description("数据库操作:执行 SQL、查看表结构、导入导出数据");
8
+ .description("数据库操作:执行 SQL、查看表结构、批量导入与导出数据")
9
+ .usage("<command> [flags]");
9
10
  dbCmd.action(() => {
10
11
  dbCmd.outputHelp();
11
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
+ `);
12
21
  dbCmd
13
22
  .command("sql")
14
- .description("执行 SQL 语句")
15
- .argument("[query]", "要执行的 SQL 语句")
16
- .option("--env <env>", "目标环境(main / dev)")
23
+ .description("执行任意 PostgreSQL 语句(DDL / DML / SELECT 等)")
24
+ .usage("[query] [flags]")
25
+ .argument("[query]", "要执行的 SQL 语句;省略时从标准输入读取")
26
+ .option("--env <env>", "目标环境(main / dev),未指定时按应用的多环境配置自动选择")
17
27
  .action(async (query, opts) => {
18
28
  await (0, index_1.handleDbSql)(query, opts);
19
- });
29
+ })
30
+ .addHelpText("after", `
31
+ Notes:
32
+ - 单条语句影响或返回的数据行数上限为 1000;超出会被拒绝执行
33
+ - 多条语句以分号分隔;任一语句失败时响应中会标明失败语句的位置(statement_index)
34
+ - 支持手写 BEGIN / COMMIT / 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
+ `);
20
54
  // schema 二级资源分组
21
55
  const schemaCmd = dbCmd
22
56
  .command("schema")
23
- .description("查看表结构");
57
+ .description("查看表结构:列出所有表、查看单张表的字段与索引")
58
+ .usage("<command> [flags]");
24
59
  schemaCmd.action(() => {
25
60
  schemaCmd.outputHelp();
26
61
  });
62
+ schemaCmd.addHelpText("after", `
63
+ Examples:
64
+ $ miaoda db schema list
65
+ $ miaoda db schema get users
66
+ $ miaoda db schema get users --ddl
67
+ $ miaoda db schema get users --json
68
+ `);
27
69
  schemaCmd
28
70
  .command("list")
29
- .description("列出当前应用的所有表(含行数估算、占用大小、列数)")
30
- .option("--env <env>", "目标环境(main / dev)")
71
+ .description("列出当前应用的所有表,包含行数、占用大小、列数等概要信息")
72
+ .usage("[flags]")
73
+ .option("--env <env>", "目标环境(main / dev),未指定时按应用的多环境配置自动选择")
31
74
  .action(async (opts) => {
32
75
  await (0, index_1.handleDbSchemaList)(opts);
33
- });
76
+ })
77
+ .addHelpText("after", `
78
+ Notes:
79
+ - rows 与 size_bytes 由 PostgreSQL 统计信息估算得出,与实际值可能存在偏差
80
+
81
+ Examples:
82
+ $ miaoda db schema list
83
+ name rows size_bytes columns
84
+ users 120 65536 6
85
+ orders 5400 327680 9
86
+
87
+ $ miaoda db schema list --json | jq '.[].name'
88
+ "users"
89
+ "orders"
90
+
91
+ $ miaoda db schema list --env main
92
+ `);
34
93
  schemaCmd
35
94
  .command("get")
36
- .description("查看单张表的字段、索引与建表语句")
37
- .argument("<table>", "表名")
38
- .option("--ddl", "输出完整建表语句而非概要")
39
- .option("--env <env>", "目标环境(main / dev)")
95
+ .description("查看单张表的字段、索引和建表语句,也可用来检测表是否存在")
96
+ .usage("<table> [flags]")
97
+ .argument("<table>", "表名(无需带 schema 前缀)")
98
+ .option("--ddl", "仅输出 CREATE TABLE 建表语句")
99
+ .option("--env <env>", "目标环境(main / dev),未指定时按应用的多环境配置自动选择")
40
100
  .action(async (table, opts) => {
41
101
  await (0, index_1.handleDbSchemaGet)(table, opts);
42
- });
102
+ })
103
+ .addHelpText("after", `
104
+ Notes:
105
+ - 检测表是否存在:成功返回(退出码 0)即为存在;错误码 TABLE_NOT_FOUND 即为不存在
106
+
107
+ Examples:
108
+ $ miaoda db schema get users
109
+ CREATE TABLE users (
110
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
111
+ ...
112
+ );
113
+
114
+ $ miaoda db schema get users --ddl
115
+ CREATE TABLE users ( ... );
116
+
117
+ $ miaoda db schema get users --json
118
+ {"name":"users","columns":[...],"indexes":[...]}
119
+
120
+ # 报错:表不存在
121
+ $ miaoda db schema get no_such
122
+ Error: TABLE_NOT_FOUND
123
+ hint: Run \`miaoda db schema list\` to see all tables.
124
+ `);
43
125
  // data 二级资源分组
44
126
  const dataCmd = dbCmd
45
127
  .command("data")
46
- .description("批量导入与导出数据");
128
+ .description("批量导入与导出表数据")
129
+ .usage("<command> [flags]");
47
130
  dataCmd.action(() => {
48
131
  dataCmd.outputHelp();
49
132
  });
133
+ dataCmd.addHelpText("after", `
134
+ Examples:
135
+ $ miaoda db data import users.csv
136
+ $ miaoda db data import data.json --table customers
137
+ $ miaoda db data export users
138
+ $ miaoda db data export users --format json
139
+ `);
50
140
  dataCmd
51
141
  .command("import")
52
- .description("把本地 CSV / JSON 文件导入到表(单次最多 5000 行 / 1 MB,全部成功或全部回滚)")
53
- .argument("<file>", "本地文件路径")
54
- .option("--table <name>", "目标表名(缺省按文件名推断)")
55
- .option("--format <fmt>", "文件格式 csv / json(缺省按扩展名推断)")
142
+ .description("将本地 CSV / JSON 文件批量导入到指定表")
143
+ .usage("<file> [flags]")
144
+ .argument("<file>", "本地文件路径(CSV 或 JSON 格式)")
145
+ .option("--table <name>", "目标表名;未指定时按文件名(不含扩展名)推断")
146
+ .option("--format <fmt>", "文件格式 csv / json;未指定时按文件扩展名推断")
56
147
  .action(async (file, opts) => {
57
148
  await (0, index_1.handleDbDataImport)(file, opts);
58
- });
149
+ })
150
+ .addHelpText("after", `
151
+ Notes:
152
+ - CSV 文件首行需为列名表头,与目标表字段名一一对应
153
+ - JSON 文件需为对象数组,每个对象的 key 与目标表字段名一一对应
154
+ - 单次最多 5000 行 / 1 MB;超出请分批导入
155
+ - 整批原子写入:任意一行失败时全部回滚,响应中包含失败行号和原因
156
+
157
+ Examples:
158
+ $ miaoda db data import users.csv
159
+ ✓ Imported 120 rows into 'users'
160
+
161
+ $ miaoda db data import data.json --table customers
162
+ ✓ Imported 240 rows into 'customers'
163
+
164
+ $ miaoda db data import dump --table orders --format csv
165
+ ✓ Imported 80 rows into 'orders'
166
+
167
+ # 报错:第 5 行格式错误,整批回滚
168
+ $ miaoda db data import broken.csv
169
+ Error: ROW_PARSE_FAILED at row 5
170
+ hint: Check column count and value types match the table schema.
171
+ `);
59
172
  dataCmd
60
173
  .command("export")
61
- .description("把整张表导出为 CSV / JSON 文件(单次最多 5000 行 / 1 MB)")
62
- .argument("<table>", "表名")
63
- .option("--format <fmt>", "导出格式 csv / json(默认 csv)")
64
- .option("-f, --file <path>", "输出文件路径(默认 <table>.<format>)")
174
+ .description("将指定表的数据导出为 CSV / JSON 文件")
175
+ .usage("<table> [flags]")
176
+ .argument("<table>", "表名(无需带 schema 前缀)")
177
+ .option("--format <fmt>", "导出格式 csv / json,默认 csv")
178
+ .option("-f, --file <path>", "输出文件路径,默认 <table>.<format>")
65
179
  .option("--limit <n>", "最多导出行数(不超过 5000)")
66
180
  .action(async (table, opts) => {
67
181
  await (0, index_1.handleDbDataExport)(table, opts);
68
- });
182
+ })
183
+ .addHelpText("after", `
184
+ Notes:
185
+ - 单次最多导出 5000 行 / 1 MB;超出请使用 --limit 控制或分批导出
186
+ - 按表的物理存储顺序导出,无固定排序;如需固定顺序,请改用 miaoda db sql 加 ORDER BY 自行写出文件
187
+
188
+ Examples:
189
+ $ miaoda db data export users
190
+ ✓ Exported 120 rows to ./users.csv
191
+
192
+ $ miaoda db data export users --format json
193
+ ✓ Exported 120 rows to ./users.json
194
+
195
+ $ miaoda db data export users -f /tmp/u.csv
196
+ ✓ Exported 120 rows to /tmp/u.csv
197
+
198
+ $ miaoda db data export users --limit 1000
199
+ ✓ Exported 1000 rows to ./users.csv
200
+ `);
69
201
  }
@@ -19,57 +19,159 @@ function parsePositiveInt(raw) {
19
19
  function registerFileCommands(program) {
20
20
  const fileCmd = program
21
21
  .command("file")
22
- .description("文件操作:上传、下载、删除、查询");
22
+ .description("文件操作:上传、下载、删除、查询")
23
+ .usage("<command> [flags]");
23
24
  fileCmd.action(() => {
24
25
  fileCmd.outputHelp();
25
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
+ `);
26
36
  fileCmd
27
37
  .command("ls")
28
- .description("列出应用下的文件")
29
- .argument("[query]", "可选筛选值:以 / 开头视为路径(精确匹配),否则按文件名匹配")
38
+ .description("列出应用下的文件,可按名称、路径、MIME 类型、大小、上传时间筛选")
39
+ .usage("[query] [flags]")
40
+ .argument("[query]", "筛选值:以 / 开头视为路径精确匹配,否则按文件名精确匹配")
30
41
  .option("--path <path>", "按路径精确匹配")
31
42
  .option("--name <name>", "按文件名精确匹配")
32
43
  .option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
33
- .option("--size-gt <size>", "文件大小下限(支持 B/KB/MB/GB)")
34
- .option("--size-lt <size>", "文件大小上限(支持 B/KB/MB/GB)")
35
- .option("--uploaded-since <time>", "上传时间下限(ISO 8601")
36
- .option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
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)
37
48
  .option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
38
49
  .option("--all", "自动翻页返回全部结果")
39
50
  .action(async (query, opts) => {
40
51
  await (0, index_1.handleFileLs)({ ...opts, query });
41
- });
52
+ })
53
+ .addHelpText("after", `
54
+ Notes:
55
+ - 默认按上传时间倒序展示
56
+ - --all 会自动翻页拉取全部结果,文件较多时耗时较长,建议先用筛选条件缩小范围
57
+
58
+ Examples:
59
+ $ miaoda file ls
60
+ path size uploaded_at
61
+ /uploads/report.pdf 1.2 MB 2026-04-20 10:00
62
+ /uploads/photo.png 800 KB 2026-04-21 14:30
63
+
64
+ $ miaoda file ls report.pdf
65
+ $ miaoda file ls /uploads/report.pdf
66
+ $ miaoda file ls --type image/png --size-gt 1MB
67
+ $ miaoda file ls --uploaded-since 2026-04-01
68
+
69
+ $ miaoda file ls --all --json | jq '.[].name'
70
+ "report.pdf"
71
+ "photo.png"
72
+ `);
42
73
  fileCmd
43
74
  .command("stat")
44
- .description("查看文件的元数据(不含下载链接,需要链接请用 sign")
45
- .argument("<file>", "文件的路径或文件名")
75
+ .description("查看文件元数据;获取下载链接请使用 miaoda file sign")
76
+ .usage("<file> [flags]")
77
+ .argument("<file>", "文件的路径或文件名(自动识别)")
46
78
  .action(async (file, opts) => {
47
79
  await (0, index_1.handleFileStat)(file, opts);
48
- });
80
+ })
81
+ .addHelpText("after", `
82
+ Notes:
83
+ - 仅返回元数据,不包含下载链接;如需分享或下载请使用 miaoda file sign 生成临时链接
84
+
85
+ Examples:
86
+ $ miaoda file stat report.pdf
87
+ path: /uploads/report.pdf
88
+ size: 1.2 MB
89
+ type: application/pdf
90
+ uploaded_at: 2026-04-20 10:00
91
+
92
+ $ miaoda file stat /uploads/report.pdf
93
+ $ miaoda file stat report.pdf --json
94
+ {"path":"/uploads/report.pdf","size":1258291,...}
95
+
96
+ # 报错:文件不存在
97
+ $ miaoda file stat no_such.pdf
98
+ Error: FILE_NOT_FOUND
99
+ hint: Run \`miaoda file ls\` to see available files.
100
+ `);
49
101
  fileCmd
50
102
  .command("cp")
51
- .description("上传或下载文件(按 src/dst 自动判断方向)")
52
- .argument("<src>", "源:本地文件路径 或 远程文件路径/名")
53
- .argument("<dst>", "目标:本地路径 远程路径")
103
+ .description("上传或下载文件,根据源和目标自动判断方向")
104
+ .usage("<src> <dst> [flags]")
105
+ .argument("<src>", "源:本地文件路径或远程文件路径 / 文件名")
106
+ .argument("<dst>", "目标:本地路径或远程路径")
54
107
  .option("--rename <name>", "上传后在远端使用的新文件名")
55
108
  .action(async (src, dst, opts) => {
56
109
  await (0, index_1.handleFileCp)(src, dst, opts);
57
- });
110
+ })
111
+ .addHelpText("after", `
112
+ Notes:
113
+ - 源是本地存在的文件时,执行上传
114
+ - 源不是本地文件、目标是本地路径时,执行下载
115
+
116
+ Examples:
117
+ $ miaoda file cp ./report.pdf /uploads/
118
+ ✓ Uploaded report.pdf → /uploads/report.pdf
119
+
120
+ $ miaoda file cp ./report.pdf /uploads/ --rename r.pdf
121
+ ✓ Uploaded report.pdf → /uploads/r.pdf
122
+
123
+ $ miaoda file cp /uploads/report.pdf .
124
+ ✓ Downloaded /uploads/report.pdf → ./report.pdf
125
+
126
+ $ miaoda file cp /uploads/report.pdf ./local.pdf
127
+ ✓ Downloaded /uploads/report.pdf → ./local.pdf
128
+ `);
58
129
  fileCmd
59
130
  .command("rm")
60
- .description("批量删除文件(单次最多 100 个,部分失败不影响其他)")
131
+ .description("批量删除文件,单条失败不影响其他文件")
132
+ .usage("[paths...] [flags]")
61
133
  .argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
62
- .option("-n, --name <name>", "按文件名删除(可多次指定)", (value, prev) => [...(prev ?? []), value])
63
- .option("-y, --yes", "跳过交互确认(脚本 / agent 调用必加)")
134
+ .option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
135
+ .option("-y, --yes", "跳过交互确认;非交互场景必加")
64
136
  .action(async (paths, opts) => {
65
137
  await (0, index_1.handleFileRm)(paths, opts);
66
- });
138
+ })
139
+ .addHelpText("after", `
140
+ Notes:
141
+ - 删除后无法恢复,请确认目标文件正确再执行
142
+ - 单次最多 100 个;超出请分批删除
143
+ - 单条失败不会影响整体执行;响应中会列出每条文件的成功 / 失败状态
144
+
145
+ Examples:
146
+ $ miaoda file rm /uploads/a.pdf /uploads/b.pdf -y
147
+ ✓ Deleted 2 files
148
+
149
+ $ miaoda file rm a.pdf b.pdf -y
150
+ ✓ Deleted 2 files
151
+
152
+ $ miaoda file rm -n a.pdf -n b.pdf -y
153
+ ✓ Deleted 2 files
154
+ `);
67
155
  fileCmd
68
156
  .command("sign")
69
- .description("生成可分享的临时下载链接")
157
+ .description("为文件生成可分享的临时下载链接")
158
+ .usage("<file> [flags]")
70
159
  .argument("<file>", "文件的路径或文件名")
71
- .option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 单位(默认 7d,最长 30d)")
160
+ .option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 等单位(默认 7d,最长 30d)")
72
161
  .action(async (file, opts) => {
73
162
  await (0, index_1.handleFileSign)(file, opts);
74
- });
163
+ })
164
+ .addHelpText("after", `
165
+ Notes:
166
+ - 链接生成后立即生效
167
+ - 有效期最长 30 天,超过会被拒绝;不传 --expires 默认 7 天
168
+ - 链接持有者即可下载文件,请仅分享给信任的对象
169
+
170
+ Examples:
171
+ $ miaoda file sign report.pdf
172
+ https://...?expires=... (valid 7d)
173
+
174
+ $ miaoda file sign report.pdf --expires 30m
175
+ $ miaoda file sign report.pdf --expires 24h
176
+ `);
75
177
  }
@@ -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
  });
@@ -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")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.0-alpha.ec1a658",
3
+ "version": "0.1.0-alpha.fccd72b",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {