@lark-apaas/miaoda-cli 0.1.0-alpha.08508f4 → 0.1.0-alpha.0d863af
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/db/api.js +65 -54
- package/dist/cli/commands/db/index.js +131 -22
- package/dist/cli/commands/file/index.js +100 -11
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/help.js +84 -0
- package/dist/main.js +8 -0
- package/dist/utils/http.js +1 -1
- package/package.json +2 -2
package/dist/api/db/api.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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,174 @@ 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
|
-
.
|
|
16
|
-
.
|
|
23
|
+
.description("执行 SQL 语句(DDL/DML/SELECT 任意 PG 语句)")
|
|
24
|
+
.usage("[query] [flags]")
|
|
25
|
+
.argument("[query]", "SQL 语句;省略时从 stdin 读取")
|
|
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
|
+
- 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
|
+
`);
|
|
20
54
|
// schema 二级资源分组
|
|
21
55
|
const schemaCmd = dbCmd
|
|
22
56
|
.command("schema")
|
|
23
|
-
.description("
|
|
57
|
+
.description("查看表结构(list / get)")
|
|
58
|
+
.usage("<command> [flags]");
|
|
24
59
|
schemaCmd.action(() => {
|
|
25
60
|
schemaCmd.outputHelp();
|
|
26
61
|
});
|
|
27
62
|
schemaCmd
|
|
28
63
|
.command("list")
|
|
29
64
|
.description("列出当前应用的所有表(含行数估算、占用大小、列数)")
|
|
30
|
-
.
|
|
65
|
+
.usage("[flags]")
|
|
66
|
+
.option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
|
|
31
67
|
.action(async (opts) => {
|
|
32
68
|
await (0, index_1.handleDbSchemaList)(opts);
|
|
33
|
-
})
|
|
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
|
+
`);
|
|
34
83
|
schemaCmd
|
|
35
84
|
.command("get")
|
|
36
|
-
.description("
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.option("--
|
|
85
|
+
.description("查看单张表的字段、索引与建表语句;判断表是否存在也用这条")
|
|
86
|
+
.usage("<table> [flags]")
|
|
87
|
+
.argument("<table>", "表名(不带 schema 前缀)")
|
|
88
|
+
.option("--ddl", "只输出 CREATE TABLE 语句,便于复制 / 备份")
|
|
89
|
+
.option("--env <env>", "目标环境(main / dev),缺省按多环境状态兜底")
|
|
40
90
|
.action(async (table, opts) => {
|
|
41
91
|
await (0, index_1.handleDbSchemaGet)(table, opts);
|
|
42
|
-
})
|
|
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
|
+
`);
|
|
43
115
|
// data 二级资源分组
|
|
44
116
|
const dataCmd = dbCmd
|
|
45
117
|
.command("data")
|
|
46
|
-
.description("
|
|
118
|
+
.description("批量导入与导出数据(import / export)")
|
|
119
|
+
.usage("<command> [flags]");
|
|
47
120
|
dataCmd.action(() => {
|
|
48
121
|
dataCmd.outputHelp();
|
|
49
122
|
});
|
|
50
123
|
dataCmd
|
|
51
124
|
.command("import")
|
|
52
125
|
.description("把本地 CSV / JSON 文件导入到表(单次最多 5000 行 / 1 MB,全部成功或全部回滚)")
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.option("--
|
|
126
|
+
.usage("<file> [flags]")
|
|
127
|
+
.argument("<file>", "本地文件路径(CSV 或 JSON)")
|
|
128
|
+
.option("--table <name>", "目标表名;缺省按文件名(去扩展名)推断")
|
|
129
|
+
.option("--format <fmt>", "文件格式 csv / json;缺省按文件扩展名推断")
|
|
56
130
|
.action(async (file, opts) => {
|
|
57
131
|
await (0, index_1.handleDbDataImport)(file, opts);
|
|
58
|
-
})
|
|
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
|
+
`);
|
|
59
153
|
dataCmd
|
|
60
154
|
.command("export")
|
|
61
155
|
.description("把整张表导出为 CSV / JSON 文件(单次最多 5000 行 / 1 MB)")
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.option("
|
|
156
|
+
.usage("<table> [flags]")
|
|
157
|
+
.argument("<table>", "表名(不带 schema 前缀)")
|
|
158
|
+
.option("--format <fmt>", "导出格式 csv / json,默认 csv")
|
|
159
|
+
.option("-f, --file <path>", "输出文件路径,默认 <table>.<format>")
|
|
65
160
|
.option("--limit <n>", "最多导出行数(不超过 5000)")
|
|
66
161
|
.action(async (table, opts) => {
|
|
67
162
|
await (0, index_1.handleDbDataExport)(table, opts);
|
|
68
|
-
})
|
|
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
|
+
`);
|
|
69
178
|
}
|
|
@@ -19,13 +19,24 @@ 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("
|
|
38
|
+
.description("列出应用下的文件,支持名称 / 路径 / MIME / 大小 / 时间多维筛选")
|
|
39
|
+
.usage("[query] [flags]")
|
|
29
40
|
.argument("[query]", "可选筛选值:以 / 开头视为路径(精确匹配),否则按文件名匹配")
|
|
30
41
|
.option("--path <path>", "按路径精确匹配")
|
|
31
42
|
.option("--name <name>", "按文件名精确匹配")
|
|
@@ -38,38 +49,116 @@ function registerFileCommands(program) {
|
|
|
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
|
+
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
|
+
`);
|
|
42
69
|
fileCmd
|
|
43
70
|
.command("stat")
|
|
44
|
-
.description("
|
|
45
|
-
.
|
|
71
|
+
.description("查看文件元数据(不含下载链接,需要链接用 sign)")
|
|
72
|
+
.usage("<file> [flags]")
|
|
73
|
+
.argument("<file>", "文件的路径或文件名(自动识别)")
|
|
46
74
|
.action(async (file, opts) => {
|
|
47
75
|
await (0, index_1.handleFileStat)(file, opts);
|
|
48
|
-
})
|
|
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
|
+
`);
|
|
49
94
|
fileCmd
|
|
50
95
|
.command("cp")
|
|
51
|
-
.description("
|
|
96
|
+
.description("上传或下载文件,按 src/dst 自动判断方向")
|
|
97
|
+
.usage("<src> <dst> [flags]")
|
|
52
98
|
.argument("<src>", "源:本地文件路径 或 远程文件路径/名")
|
|
53
99
|
.argument("<dst>", "目标:本地路径 或 远程路径")
|
|
54
100
|
.option("--rename <name>", "上传后在远端使用的新文件名")
|
|
55
101
|
.action(async (src, dst, opts) => {
|
|
56
102
|
await (0, index_1.handleFileCp)(src, dst, opts);
|
|
57
|
-
})
|
|
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
|
+
`);
|
|
58
122
|
fileCmd
|
|
59
123
|
.command("rm")
|
|
60
124
|
.description("批量删除文件(单次最多 100 个,部分失败不影响其他)")
|
|
125
|
+
.usage("[paths...] [flags]")
|
|
61
126
|
.argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
|
|
62
|
-
.option("-n, --name <name>", "
|
|
127
|
+
.option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
|
|
63
128
|
.option("-y, --yes", "跳过交互确认(脚本 / agent 调用必加)")
|
|
64
129
|
.action(async (paths, opts) => {
|
|
65
130
|
await (0, index_1.handleFileRm)(paths, opts);
|
|
66
|
-
})
|
|
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
|
+
`);
|
|
67
147
|
fileCmd
|
|
68
148
|
.command("sign")
|
|
69
149
|
.description("生成可分享的临时下载链接")
|
|
150
|
+
.usage("<file> [flags]")
|
|
70
151
|
.argument("<file>", "文件的路径或文件名")
|
|
71
152
|
.option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 单位(默认 7d,最长 30d)")
|
|
72
153
|
.action(async (file, opts) => {
|
|
73
154
|
await (0, index_1.handleFileSign)(file, opts);
|
|
74
|
-
})
|
|
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
|
+
`);
|
|
75
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
|
});
|
package/dist/cli/help.js
ADDED
|
@@ -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/dist/utils/http.js
CHANGED
|
@@ -12,7 +12,7 @@ let runtimeClient;
|
|
|
12
12
|
/**
|
|
13
13
|
* 获取单例 HttpClient(默认:管理端 innerapi)。
|
|
14
14
|
*
|
|
15
|
-
* 管理端链路仅适用于妙搭开发态——baseURL 从 `
|
|
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.
|
|
3
|
+
"version": "0.1.0-alpha.0d863af",
|
|
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.
|
|
28
|
+
"@lark-apaas/http-client": "^0.1.5",
|
|
29
29
|
"commander": "^13.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|