@lark-apaas/miaoda-cli 0.1.0 → 0.1.1
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 +211 -0
- package/dist/api/db/client.js +166 -0
- package/dist/api/db/index.js +16 -0
- package/dist/api/db/parsers.js +161 -0
- package/dist/api/db/types.js +10 -0
- package/dist/api/file/api.js +467 -0
- package/dist/api/file/client.js +199 -0
- package/dist/api/file/detect.js +56 -0
- package/dist/api/file/index.js +18 -0
- package/dist/api/file/parsers.js +72 -0
- package/dist/api/file/types.js +3 -0
- package/dist/api/index.js +5 -1
- package/dist/api/plugin/api.js +3 -3
- package/dist/cli/commands/db/index.js +208 -0
- package/dist/cli/commands/file/index.js +212 -0
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +7 -8
- package/dist/cli/handlers/db/data.js +171 -0
- package/dist/cli/handlers/db/index.js +11 -0
- package/dist/cli/handlers/db/schema.js +163 -0
- package/dist/cli/handlers/db/sql.js +367 -0
- package/dist/cli/handlers/file/cp.js +220 -0
- package/dist/cli/handlers/file/index.js +13 -0
- package/dist/cli/handlers/file/ls.js +111 -0
- package/dist/cli/handlers/file/rm.js +263 -0
- package/dist/cli/handlers/file/sign.js +96 -0
- package/dist/cli/handlers/file/stat.js +97 -0
- package/dist/cli/handlers/index.js +2 -0
- package/dist/cli/help.js +188 -0
- package/dist/main.js +9 -1
- package/dist/utils/error.js +3 -0
- package/dist/utils/http.js +31 -10
- package/dist/utils/index.js +3 -1
- package/dist/utils/output.js +18 -5
- package/dist/utils/render.js +187 -0
- package/package.json +2 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 判断用户输入应当作为 **path**(后端存储 key)还是 **file_name** 处理。
|
|
4
|
+
*
|
|
5
|
+
* 背景:
|
|
6
|
+
* - file-storage 的 `filePath` 是 bucket 内 unique 的对象存储 key,既可能是用户在
|
|
7
|
+
* upload 时指定的自定义路径(如 `logs/2026/data.csv`),也可能是后端 preUpload
|
|
8
|
+
* 自动生成的 fileKey(固定长度的 alphanumeric,可带扩展名,如 `abcdef1234567890.png`)
|
|
9
|
+
* - `file_name` 是上传时随文件一起登记的人类可读名字(可以含空格 / 中文 / 重复)
|
|
10
|
+
*
|
|
11
|
+
* CLI 默认让用户直接传一个字符串,不强制加 `--name`。当字符串"看起来像后端 key"时
|
|
12
|
+
* 走 path 语义(HEAD 直连),否则走 file_name 语义(FilterExpression `name eq`)。
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.looksLikePath = looksLikePath;
|
|
16
|
+
exports.toAbsolutePath = toAbsolutePath;
|
|
17
|
+
/**
|
|
18
|
+
* 后端 preUpload 生成的 fileKey 特征(实测 file-storage 响应):
|
|
19
|
+
* - base 段是 **16+ 位纯十进制数字**(snowflake ID,如 `1863152526384148`)
|
|
20
|
+
* - 可选 1~2 段扩展名(`.ext` / `.tar.gz`),每段 ≤ 10 位字母或数字
|
|
21
|
+
*
|
|
22
|
+
* 例:
|
|
23
|
+
* - `1863152526384148` ✓ (16 位 snowflake)
|
|
24
|
+
* - `1863152526384148.jpg` ✓
|
|
25
|
+
* - `1858533158493259.tar.gz` ✓ (2 段扩展)
|
|
26
|
+
* - `618a80ddb3b7e1496.jpg` ✗ (含字母,是用户起的 file_name)
|
|
27
|
+
* - `1234` ✗ (位数不够)
|
|
28
|
+
* - `报告.xlsx` / `my file.doc` ✗ (非 ASCII / 含空格)
|
|
29
|
+
*
|
|
30
|
+
* 修紧到纯数字是为了避开"用户上传时把文件命成 16+ 位 hex"的真实场景 ——
|
|
31
|
+
* 后端 fileKey 至今都是纯十进制,二者不重叠。
|
|
32
|
+
*/
|
|
33
|
+
const BACKEND_KEY_PATTERN = /^\d{16,}(?:\.[a-zA-Z0-9]{1,10}){0,2}$/;
|
|
34
|
+
/**
|
|
35
|
+
* 输入"看起来像 path"的判断:
|
|
36
|
+
* 1. 以 `/` 开头 → 显式绝对路径
|
|
37
|
+
* 2. 含 `/`(但不以 `/` 开头)→ 命名空间形式的 key,如 `logs/2026/data.csv`
|
|
38
|
+
* 3. 匹配 `BACKEND_KEY_PATTERN` → 后端生成的 fileKey
|
|
39
|
+
* 其他情况一律视为 file_name。
|
|
40
|
+
*/
|
|
41
|
+
function looksLikePath(input) {
|
|
42
|
+
if (!input)
|
|
43
|
+
return false;
|
|
44
|
+
if (input.startsWith("/"))
|
|
45
|
+
return true;
|
|
46
|
+
if (input.includes("/"))
|
|
47
|
+
return true;
|
|
48
|
+
return BACKEND_KEY_PATTERN.test(input);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 规整为带前导 `/` 的展示/HEAD 形式。
|
|
52
|
+
* 便于从 auto-detected 的 "abc123...xyz.txt" 回到统一的 `/abc123...xyz.txt`。
|
|
53
|
+
*/
|
|
54
|
+
function toAbsolutePath(input) {
|
|
55
|
+
return input.startsWith("/") ? input : `/${input}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
|
|
4
|
+
var api_1 = require("./api");
|
|
5
|
+
Object.defineProperty(exports, "listFiles", { enumerable: true, get: function () { return api_1.listFiles; } });
|
|
6
|
+
Object.defineProperty(exports, "statFile", { enumerable: true, get: function () { return api_1.statFile; } });
|
|
7
|
+
Object.defineProperty(exports, "uploadFile", { enumerable: true, get: function () { return api_1.uploadFile; } });
|
|
8
|
+
Object.defineProperty(exports, "signDownload", { enumerable: true, get: function () { return api_1.signDownload; } });
|
|
9
|
+
Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function () { return api_1.downloadFile; } });
|
|
10
|
+
Object.defineProperty(exports, "deleteFiles", { enumerable: true, get: function () { return api_1.deleteFiles; } });
|
|
11
|
+
Object.defineProperty(exports, "resolveInputs", { enumerable: true, get: function () { return api_1.resolveInputs; } });
|
|
12
|
+
Object.defineProperty(exports, "parseTimeFilterMs", { enumerable: true, get: function () { return api_1.parseTimeFilterMs; } });
|
|
13
|
+
var client_1 = require("./client");
|
|
14
|
+
Object.defineProperty(exports, "getDefaultBucketId", { enumerable: true, get: function () { return client_1.getDefaultBucketId; } });
|
|
15
|
+
Object.defineProperty(exports, "resetBucketCache", { enumerable: true, get: function () { return client_1.resetBucketCache; } });
|
|
16
|
+
var detect_1 = require("./detect");
|
|
17
|
+
Object.defineProperty(exports, "looksLikePath", { enumerable: true, get: function () { return detect_1.looksLikePath; } });
|
|
18
|
+
Object.defineProperty(exports, "toAbsolutePath", { enumerable: true, get: function () { return detect_1.toAbsolutePath; } });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAttachment = parseAttachment;
|
|
4
|
+
exports.parseHead = parseHead;
|
|
5
|
+
/** 安全读取字符串字段(空字符串回退为默认值) */
|
|
6
|
+
function str(value, fallback = "") {
|
|
7
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
8
|
+
}
|
|
9
|
+
/** 安全读取数字字段(非正值回退到 0) */
|
|
10
|
+
function int(value) {
|
|
11
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
12
|
+
return Math.trunc(value);
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
function readSize(meta) {
|
|
17
|
+
if (!meta)
|
|
18
|
+
return 0;
|
|
19
|
+
const v = meta.size ??
|
|
20
|
+
meta.fileSize ??
|
|
21
|
+
meta.file_size ??
|
|
22
|
+
meta.contentLength ??
|
|
23
|
+
0;
|
|
24
|
+
return int(v);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 展示层 path 统一带前导 `/`(对齐 PRD / SKILL 里的示例)。
|
|
28
|
+
* 后端存储的 path 实际是无前导 `/` 的,这里补齐。
|
|
29
|
+
*/
|
|
30
|
+
function toDisplayPath(p) {
|
|
31
|
+
if (!p)
|
|
32
|
+
return p;
|
|
33
|
+
return p.startsWith("/") ? p : `/${p}`;
|
|
34
|
+
}
|
|
35
|
+
/** 将后端 attachment 结构映射为 CLI FileInfo(用于 ls) */
|
|
36
|
+
function parseAttachment(att) {
|
|
37
|
+
const rawPath = str(att.filePath, str(att.name));
|
|
38
|
+
const info = {
|
|
39
|
+
file_name: str(att.name),
|
|
40
|
+
path: toDisplayPath(rawPath),
|
|
41
|
+
size_bytes: readSize(att.metadata),
|
|
42
|
+
type: str(att.metadata?.mimeType),
|
|
43
|
+
uploaded_at: str(att.createdAt),
|
|
44
|
+
};
|
|
45
|
+
const downloadUrl = str(att.downloadURL);
|
|
46
|
+
if (downloadUrl)
|
|
47
|
+
info.download_url = downloadUrl;
|
|
48
|
+
// uploaded_by 从 createdBy.name 映射(后端返 {userID, name, email, ...},空代表匿名)
|
|
49
|
+
const uploader = str(att.createdBy?.name);
|
|
50
|
+
if (uploader)
|
|
51
|
+
info.uploaded_by = uploader;
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
54
|
+
/** 将后端 head 响应映射为 CLI FileInfo(用于 stat) */
|
|
55
|
+
function parseHead(data, filePath) {
|
|
56
|
+
const outer = data?.metadata ?? {};
|
|
57
|
+
const inner = outer.metadata ?? {};
|
|
58
|
+
const info = {
|
|
59
|
+
file_name: str(outer.name),
|
|
60
|
+
path: toDisplayPath(filePath || str(outer.name)),
|
|
61
|
+
size_bytes: readSize(inner),
|
|
62
|
+
type: str(inner.mimeType),
|
|
63
|
+
uploaded_at: str(inner.lastModified),
|
|
64
|
+
};
|
|
65
|
+
const downloadUrl = str(outer.downloadURL);
|
|
66
|
+
if (downloadUrl)
|
|
67
|
+
info.download_url = downloadUrl;
|
|
68
|
+
const uploader = str(outer.createdBy?.name);
|
|
69
|
+
if (uploader)
|
|
70
|
+
info.uploaded_by = uploader;
|
|
71
|
+
return info;
|
|
72
|
+
}
|
package/dist/api/index.js
CHANGED
|
@@ -33,6 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.plugin = void 0;
|
|
36
|
+
exports.db = exports.file = exports.plugin = void 0;
|
|
37
37
|
const _plugin = __importStar(require("../api/plugin/index"));
|
|
38
|
+
const _file = __importStar(require("../api/file/index"));
|
|
39
|
+
const _db = __importStar(require("../api/db/index"));
|
|
38
40
|
exports.plugin = { ..._plugin };
|
|
41
|
+
exports.file = { ..._file };
|
|
42
|
+
exports.db = { ..._db };
|
package/dist/api/plugin/api.js
CHANGED
|
@@ -22,7 +22,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
// ── Plugin Version API ──
|
|
24
24
|
async function getPluginVersions(keys, latestOnly = true) {
|
|
25
|
-
const client = (0, http_1.
|
|
25
|
+
const client = (0, http_1.getRuntimeHttpClient)();
|
|
26
26
|
const response = await client.post(`/api/v1/studio/innerapi/plugins/-/versions/batch_get?keys=${keys.join(",")}&latest_only=${String(latestOnly)}`);
|
|
27
27
|
if (!response.ok) {
|
|
28
28
|
throw new error_1.HttpError(response.status, response.url, `Failed to get plugin versions: ${String(response.status)} ${response.statusText}`);
|
|
@@ -58,7 +58,7 @@ function parsePluginKey(key) {
|
|
|
58
58
|
return { scope: match[1], name: match[2] };
|
|
59
59
|
}
|
|
60
60
|
async function downloadFromInner(pluginKey, version) {
|
|
61
|
-
const client = (0, http_1.
|
|
61
|
+
const client = (0, http_1.getRuntimeHttpClient)();
|
|
62
62
|
const { scope, name } = parsePluginKey(pluginKey);
|
|
63
63
|
const url = `/api/v1/studio/innerapi/plugins/${scope}/${name}/versions/${version}/package`;
|
|
64
64
|
const response = await client.get(url);
|
|
@@ -203,7 +203,7 @@ async function reportEvents(events) {
|
|
|
203
203
|
if (events.length === 0)
|
|
204
204
|
return true;
|
|
205
205
|
try {
|
|
206
|
-
const client = (0, http_1.
|
|
206
|
+
const client = (0, http_1.getRuntimeHttpClient)();
|
|
207
207
|
const response = await client.post("/api/v1/studio/innerapi/resource_events", { events });
|
|
208
208
|
if (!response.ok) {
|
|
209
209
|
(0, logger_1.log)("telemetry", `Failed to report events: ${String(response.status)} ${response.statusText}`);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerDbCommands = registerDbCommands;
|
|
4
|
+
const index_1 = require("../../../cli/handlers/db/index");
|
|
5
|
+
function registerDbCommands(program) {
|
|
6
|
+
const dbCmd = program
|
|
7
|
+
.command("db")
|
|
8
|
+
.description("应用数据库(PostgreSQL)的命令行操作集合。")
|
|
9
|
+
.usage("<command> [flags]")
|
|
10
|
+
// --env 注册在 db 父级,spec 把它列入 db --help 的 Global Flags;
|
|
11
|
+
// leaf 命令仍各自接收 --env 值(commander 解析时父级 option 自动适用于子命令)
|
|
12
|
+
.option("--env <name>", "指定目标环境(dev / online,仅专家模式应用支持)");
|
|
13
|
+
dbCmd.action(() => {
|
|
14
|
+
dbCmd.outputHelp();
|
|
15
|
+
});
|
|
16
|
+
dbCmd
|
|
17
|
+
.command("sql")
|
|
18
|
+
.summary("执行任意 SQL(SELECT / DML / DDL,支持多条分号分隔)")
|
|
19
|
+
.description("执行 SQL 语句,支持 SELECT、DML、DDL 等所有标准 PostgreSQL 操作。\n" +
|
|
20
|
+
"支持通过 stdin 读取(miaoda db sql < file.sql),多条语句以分号分隔。\n" +
|
|
21
|
+
"事务控制使用 PG 原生的 BEGIN / COMMIT / ROLLBACK 语法。")
|
|
22
|
+
.usage("<query> [flags]")
|
|
23
|
+
.argument("[query]", "要执行的 SQL 语句;省略时从标准输入读取")
|
|
24
|
+
.action(async function (query) {
|
|
25
|
+
await (0, index_1.handleDbSql)(query, this.optsWithGlobals());
|
|
26
|
+
})
|
|
27
|
+
.addHelpText("after", `
|
|
28
|
+
Notes:
|
|
29
|
+
- SELECT 结果超过 1000 行直接报错(RESULT_SET_TOO_LARGE),不做隐式截断。
|
|
30
|
+
需要分页时请在 SQL 中用 LIMIT / OFFSET。
|
|
31
|
+
- 多条语句不自动包事务:失败时后续不执行、已成功的不回滚。
|
|
32
|
+
需要原子性请显式 BEGIN; ...; COMMIT;
|
|
33
|
+
- --env 仅在专家模式应用且已 migration init 后可用。
|
|
34
|
+
- NULL 值在 --json 中是 JSON 原生 null,pretty / 管道中是字面字符串 NULL。
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
$ miaoda db sql "SELECT id, name, age FROM users LIMIT 10"
|
|
38
|
+
id name age
|
|
39
|
+
1001 Alice 28
|
|
40
|
+
1002 Bob 35
|
|
41
|
+
|
|
42
|
+
$ miaoda db sql "INSERT INTO users (name, age) VALUES ('Dave', 25)"
|
|
43
|
+
✓ 1 row inserted
|
|
44
|
+
|
|
45
|
+
$ miaoda db sql < migration.sql
|
|
46
|
+
Statement 1: ✓ CREATE TABLE orders
|
|
47
|
+
Statement 2: ✓ 10 rows inserted
|
|
48
|
+
✓ 2 statements executed
|
|
49
|
+
|
|
50
|
+
# 报错:SQL 语法错误
|
|
51
|
+
$ miaoda db sql "SELCT * FROM users"
|
|
52
|
+
Error: Syntax error at or near "SELCT"
|
|
53
|
+
hint: Check SQL keyword spelling. Did you mean "SELECT"?
|
|
54
|
+
|
|
55
|
+
# 报错:结果超过 1000 行
|
|
56
|
+
$ miaoda db sql "SELECT * FROM users"
|
|
57
|
+
Error: Result set exceeds the 1000-row limit (query would return 15234 rows)
|
|
58
|
+
hint: Add \`LIMIT <n>\` to your SQL to narrow the result.
|
|
59
|
+
`);
|
|
60
|
+
// schema 二级资源分组
|
|
61
|
+
const schemaCmd = dbCmd
|
|
62
|
+
.command("schema")
|
|
63
|
+
.summary("查看数据库表结构")
|
|
64
|
+
.description("查看应用数据库的表结构信息。")
|
|
65
|
+
.usage("<command> [flags]");
|
|
66
|
+
schemaCmd.action(() => {
|
|
67
|
+
schemaCmd.outputHelp();
|
|
68
|
+
});
|
|
69
|
+
schemaCmd
|
|
70
|
+
.command("list")
|
|
71
|
+
.summary("列出所有表的概览(表名、行数、大小等)")
|
|
72
|
+
.description("列出当前应用所有表的概览信息:表名、描述、行数、大小、列数、最近更新时间。\n" +
|
|
73
|
+
"查看单表完整结构请用 `schema get`;查看 DDL 变更历史请用 `changelog`(P1)。")
|
|
74
|
+
.usage("[flags]")
|
|
75
|
+
.action(async function () {
|
|
76
|
+
await (0, index_1.handleDbSchemaList)(this.optsWithGlobals());
|
|
77
|
+
})
|
|
78
|
+
.addHelpText("after", `
|
|
79
|
+
Examples:
|
|
80
|
+
$ miaoda db schema list
|
|
81
|
+
name description rows size columns updated_at
|
|
82
|
+
users 用户信息 1523 2.1 MB 5 3h ago
|
|
83
|
+
orders 订单记录 8891 12.4 MB 8 2d ago
|
|
84
|
+
products 商品目录 342 512 KB 6 2026-03-15
|
|
85
|
+
|
|
86
|
+
$ miaoda db schema list --json name,rows
|
|
87
|
+
{
|
|
88
|
+
"data": [
|
|
89
|
+
{"name": "users", "rows": 1523},
|
|
90
|
+
{"name": "orders", "rows": 8891},
|
|
91
|
+
{"name": "products", "rows": 342}
|
|
92
|
+
],
|
|
93
|
+
"next_cursor": null,
|
|
94
|
+
"has_more": false
|
|
95
|
+
}
|
|
96
|
+
`);
|
|
97
|
+
schemaCmd
|
|
98
|
+
.command("get")
|
|
99
|
+
.summary("查看单表完整结构(列定义、索引、约束)")
|
|
100
|
+
.description("查看指定表的完整结构:列定义(含类型、可空、默认值、注释)、索引、行数、大小。\n" +
|
|
101
|
+
"默认 pretty 输出 CREATE TABLE 的 SQL 文本;--json 输出结构化字段。")
|
|
102
|
+
.usage("<table> [flags]")
|
|
103
|
+
.argument("<table>", "表名(无需带 schema 前缀)")
|
|
104
|
+
.option("--ddl", "强制输出 CREATE TABLE 建表语句(pretty 默认就是 DDL,--json 时配合此 flag 返 SQL 文本)")
|
|
105
|
+
.action(async function (table) {
|
|
106
|
+
await (0, index_1.handleDbSchemaGet)(table, this.optsWithGlobals());
|
|
107
|
+
})
|
|
108
|
+
.addHelpText("after", `
|
|
109
|
+
Examples:
|
|
110
|
+
$ miaoda db schema get users
|
|
111
|
+
CREATE TABLE users (
|
|
112
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
113
|
+
name varchar(100) NOT NULL,
|
|
114
|
+
email varchar(255),
|
|
115
|
+
age integer,
|
|
116
|
+
PRIMARY KEY (id),
|
|
117
|
+
UNIQUE (email)
|
|
118
|
+
);
|
|
119
|
+
COMMENT ON TABLE users IS '用户信息';
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
# 报错:表不存在
|
|
123
|
+
$ miaoda db schema get userss
|
|
124
|
+
Error: Table 'userss' does not exist
|
|
125
|
+
hint: Did you mean 'users'? Run \`miaoda db schema list\` to see all tables.
|
|
126
|
+
`);
|
|
127
|
+
// data 二级资源分组
|
|
128
|
+
const dataCmd = dbCmd
|
|
129
|
+
.command("data")
|
|
130
|
+
.summary("表数据导入导出")
|
|
131
|
+
.description("表数据的批量导入导出,适合数据备份、跨环境迁移、外部分析。")
|
|
132
|
+
.usage("<command> [flags]");
|
|
133
|
+
dataCmd.action(() => {
|
|
134
|
+
dataCmd.outputHelp();
|
|
135
|
+
});
|
|
136
|
+
dataCmd
|
|
137
|
+
.command("import")
|
|
138
|
+
.summary("从本地 CSV / JSON 文件导入数据到表")
|
|
139
|
+
.description("从本地 CSV / JSON 文件导入数据到表。整个导入是原子的——遇到任何错误(主键冲突、\n" +
|
|
140
|
+
"类型不匹配、列名不匹配等)会回滚已插入的所有行。\n" +
|
|
141
|
+
"不支持 SQL 文件,SQL 备份请用 `miaoda db sql < <文件>.sql` 回放。")
|
|
142
|
+
.usage("<file> [flags]")
|
|
143
|
+
.argument("<file>", "本地文件路径(CSV 或 JSON 格式)")
|
|
144
|
+
.option("--table <name>", "目标表名;未指定时按文件名(不含扩展名)推断(如 users.csv → users)")
|
|
145
|
+
.option("--format <fmt>", "文件格式 csv / json;未指定时按文件扩展名推断")
|
|
146
|
+
.action(async function (file) {
|
|
147
|
+
await (0, index_1.handleDbDataImport)(file, this.optsWithGlobals());
|
|
148
|
+
})
|
|
149
|
+
.addHelpText("after", `
|
|
150
|
+
Notes:
|
|
151
|
+
- 目标表必须已存在;CSV 表头 / JSON 顶层 key 必须与表字段名完全一致。
|
|
152
|
+
- 导入为原子操作:遇到错误回滚所有已插入行,不会出现部分导入的中间状态。
|
|
153
|
+
- 仅支持 .csv 和 .json 文件扩展名。
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
$ miaoda db data import users.csv
|
|
157
|
+
✓ Imported users.csv → table 'users' (1523 rows)
|
|
158
|
+
|
|
159
|
+
$ miaoda db data import data.csv --table users
|
|
160
|
+
✓ Imported data.csv → table 'users' (1523 rows)
|
|
161
|
+
|
|
162
|
+
# 报错:CSV 表头与字段不匹配
|
|
163
|
+
$ miaoda db data import users.csv
|
|
164
|
+
Error: Column 'full_name' in CSV does not match any column in table 'users'
|
|
165
|
+
hint: Expected columns: id, name, email, age. Check CSV header row.
|
|
166
|
+
|
|
167
|
+
# 报错:主键冲突(已回滚)
|
|
168
|
+
$ miaoda db data import users.csv
|
|
169
|
+
Error: Primary key conflict at row 42 (id=1001 already exists), 0 rows imported
|
|
170
|
+
hint: Deduplicate input data, or remove conflicting rows first with
|
|
171
|
+
\`miaoda db sql "DELETE FROM users WHERE id IN (...)"\`.
|
|
172
|
+
`);
|
|
173
|
+
dataCmd
|
|
174
|
+
.command("export")
|
|
175
|
+
.summary("导出表数据到本地 CSV / JSON / SQL 文件")
|
|
176
|
+
.description("把指定表的所有数据导出到本地文件,支持 CSV / JSON / SQL 三种格式。")
|
|
177
|
+
.usage("<table> [flags]")
|
|
178
|
+
.argument("<table>", "表名(无需带 schema 前缀)")
|
|
179
|
+
.option("--format <fmt>", "导出格式 csv / json / sql,默认 csv(sql 输出 INSERT 语句,可用 db sql < file.sql 回放)")
|
|
180
|
+
.option("-f, --file <path>", "输出文件路径,默认 <表名>.<格式>")
|
|
181
|
+
.option("--limit <n>", "最多导出行数(不超过 5000)")
|
|
182
|
+
.action(async function (table) {
|
|
183
|
+
await (0, index_1.handleDbDataExport)(table, this.optsWithGlobals());
|
|
184
|
+
})
|
|
185
|
+
.addHelpText("after", `
|
|
186
|
+
Notes:
|
|
187
|
+
- SQL 格式生成 INSERT 语句,可用 \`miaoda db sql < <文件>.sql\` 在其他环境回放。
|
|
188
|
+
- 文件已存在时默认报错(FILE_ALREADY_EXISTS),用 --force 覆盖或 -f 改路径。
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
$ miaoda db data export users
|
|
192
|
+
✓ Exported users → users.csv (1523 rows)
|
|
193
|
+
|
|
194
|
+
$ miaoda db data export users --format json
|
|
195
|
+
✓ Exported users → users.json (1523 rows)
|
|
196
|
+
|
|
197
|
+
$ miaoda db data export users --format sql -f users_backup.sql
|
|
198
|
+
✓ Exported users → users_backup.sql (1523 rows)
|
|
199
|
+
|
|
200
|
+
$ miaoda db data export users -f ~/Desktop/users.csv
|
|
201
|
+
✓ Exported users → ~/Desktop/users.csv (1523 rows)
|
|
202
|
+
|
|
203
|
+
# 报错:文件已存在
|
|
204
|
+
$ miaoda db data export users
|
|
205
|
+
Error: Output file 'users.csv' already exists
|
|
206
|
+
hint: Use -f to specify a different path, or --force to overwrite.
|
|
207
|
+
`);
|
|
208
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFileCommands = registerFileCommands;
|
|
4
|
+
const index_1 = require("../../../cli/handlers/file/index");
|
|
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
|
+
}
|
|
19
|
+
function registerFileCommands(program) {
|
|
20
|
+
const fileCmd = program
|
|
21
|
+
.command("file")
|
|
22
|
+
.description("应用文件存储(TOS)的命令行操作集合。操作对象是 UGC 资源(用户上传的文件、应用运行时\n" +
|
|
23
|
+
"生成的报表 / 导出文件等),不涉及代码仓库里的本地文件。")
|
|
24
|
+
.usage("<command> [flags]");
|
|
25
|
+
fileCmd.action(() => {
|
|
26
|
+
fileCmd.outputHelp();
|
|
27
|
+
});
|
|
28
|
+
fileCmd
|
|
29
|
+
.command("ls")
|
|
30
|
+
.summary("列出 / 筛选文件")
|
|
31
|
+
.description("列出当前应用存储里的文件,支持按文件名、大小、上传时间筛选,以及游标分页。\n" +
|
|
32
|
+
"默认 pretty 输出省略 download_url(列宽限制),需获取请用 --json。")
|
|
33
|
+
.usage("[query] [flags]")
|
|
34
|
+
.argument("[query]", "筛选值:以 / 开头视为路径精确匹配,否则按文件名精确匹配")
|
|
35
|
+
.option("--path <path>", "按路径精确匹配")
|
|
36
|
+
.option("--name <name>", "按文件名精确匹配")
|
|
37
|
+
.option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
|
|
38
|
+
.option("--size-gt <size>", "文件大小下限(支持 B / KB / MB / GB)")
|
|
39
|
+
.option("--size-lt <size>", "文件大小上限(支持 B / KB / MB / GB)")
|
|
40
|
+
.option("--uploaded-since <time>", "上传时间下限(晚于该时间),支持:相对时间 1h / 2d / 1w;日期 YYYY-MM-DD;ISO 8601 如 2026-04-01T10:00:00Z")
|
|
41
|
+
.option("--uploaded-until <time>", "上传时间上限(早于该时间),支持:相对时间 1h / 2d / 1w;日期 YYYY-MM-DD;ISO 8601 如 2026-04-01T10:00:00Z")
|
|
42
|
+
.option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
|
|
43
|
+
.option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
|
|
44
|
+
.option("--all", "自动翻页返回全部结果")
|
|
45
|
+
.action(async (query, opts) => {
|
|
46
|
+
await (0, index_1.handleFileLs)({ ...opts, query });
|
|
47
|
+
})
|
|
48
|
+
.addHelpText("after", `
|
|
49
|
+
Examples:
|
|
50
|
+
$ miaoda file ls
|
|
51
|
+
file_name path size type uploaded_at
|
|
52
|
+
logo.png /images/brand/1858537546760216.png 24 KB image/png 3h ago
|
|
53
|
+
hero.jpg /images/1858537546760217.jpg 128 KB image/jpeg 2d ago
|
|
54
|
+
report.pdf /docs/1858537546760218.pdf 2.1 MB application/pdf 2026-04-10
|
|
55
|
+
|
|
56
|
+
$ miaoda file ls --name logo.png
|
|
57
|
+
file_name path size type uploaded_at
|
|
58
|
+
logo.png /images/brand/1858537546760216.png 24 KB image/png 3h ago
|
|
59
|
+
|
|
60
|
+
$ miaoda file ls --uploaded-since 7d --size-gt 1MB
|
|
61
|
+
...
|
|
62
|
+
`);
|
|
63
|
+
fileCmd
|
|
64
|
+
.command("stat")
|
|
65
|
+
.summary("查看单文件元数据")
|
|
66
|
+
.description("查看单文件完整元数据,含 download_url(应用内消费)。\n" +
|
|
67
|
+
"需要公网可访问的临时链接请用 `file sign` 生成 signed_url。")
|
|
68
|
+
.usage("<file> [flags]")
|
|
69
|
+
.argument("<file>", "文件的路径或文件名(自动识别)")
|
|
70
|
+
.action(async (file, opts) => {
|
|
71
|
+
await (0, index_1.handleFileStat)(file, opts);
|
|
72
|
+
})
|
|
73
|
+
.addHelpText("after", `
|
|
74
|
+
Notes:
|
|
75
|
+
- <file> 可传 path(推荐,全局唯一)或 file_name。
|
|
76
|
+
- file_name 重名时报 AMBIGUOUS_FILE_NAME,需先 \`ls --name\` 拿到 path 再调用。
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
$ miaoda file stat /images/brand/1858537546760216.png
|
|
80
|
+
file_name: logo.png
|
|
81
|
+
path: /images/brand/1858537546760216.png
|
|
82
|
+
size: 24 KB (24580 bytes)
|
|
83
|
+
type: image/png
|
|
84
|
+
uploaded_by: alice
|
|
85
|
+
uploaded_at: 2026-04-15 10:30:00
|
|
86
|
+
download_url: /spark/app/.../1858537546760216.png
|
|
87
|
+
|
|
88
|
+
# 报错:file_name 多匹配
|
|
89
|
+
$ miaoda file stat logo.png
|
|
90
|
+
Error: Multiple files match name 'logo.png' (2 found)
|
|
91
|
+
hint: Use path instead. Run \`miaoda file ls --name logo.png\` to see candidates.
|
|
92
|
+
`);
|
|
93
|
+
fileCmd
|
|
94
|
+
.command("cp")
|
|
95
|
+
.summary("上传或下载文件(方向由路径前缀自动判断)")
|
|
96
|
+
.description("上传或下载文件,方向由 src/dst 路径前缀自动判断:\n" +
|
|
97
|
+
" / 开头 → 远程 TOS 路径\n" +
|
|
98
|
+
" ./、~/、裸文件名 → 本地路径")
|
|
99
|
+
.usage("<src> <dst> [flags]")
|
|
100
|
+
.argument("<src>", "源:本地文件路径或远程文件路径 / 文件名")
|
|
101
|
+
.argument("<dst>", "目标:本地路径或远程路径")
|
|
102
|
+
.option("--rename <name>", "上传后在远端使用的新文件名")
|
|
103
|
+
.action(async (src, dst, opts) => {
|
|
104
|
+
await (0, index_1.handleFileCp)(src, dst, opts);
|
|
105
|
+
})
|
|
106
|
+
.addHelpText("after", `
|
|
107
|
+
Notes:
|
|
108
|
+
- 单文件上限 100 MB,超过请拆分或用 web console 上传。
|
|
109
|
+
- 同目录下允许同 file_name 并存(每次上传生成新的 path),不会因重名失败。
|
|
110
|
+
- 下载时 src 必须是完整 path(裸 file_name 会被识别为本地路径)。
|
|
111
|
+
- 上传成功返回 download_url,可写入数据库 / 应用代码作为永久引用。
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
# 上传:本地 → 远程
|
|
115
|
+
$ miaoda file cp ./logo.png /images/brand/
|
|
116
|
+
✓ Uploaded logo.png → /images/brand/1858537546760216.png
|
|
117
|
+
file_name: logo.png
|
|
118
|
+
path: /images/brand/1858537546760216.png
|
|
119
|
+
size: 24 KB (24580 bytes)
|
|
120
|
+
type: image/png
|
|
121
|
+
download_url: /spark/app/.../1858537546760216.png
|
|
122
|
+
|
|
123
|
+
# 下载:远程 → 本地
|
|
124
|
+
$ miaoda file cp /images/brand/1858537546760216.png ~/Desktop/
|
|
125
|
+
✓ Downloaded 1858537546760216.png → ~/Desktop/logo.png (24 KB)
|
|
126
|
+
|
|
127
|
+
# 上传时改名
|
|
128
|
+
$ miaoda file cp ./photo.png /images/ --rename avatar.png
|
|
129
|
+
✓ Uploaded photo.png → /images/1858537546760301.png
|
|
130
|
+
|
|
131
|
+
# 报错:本地源文件不存在
|
|
132
|
+
$ miaoda file cp ./missing.png /images/
|
|
133
|
+
Error: Local file './missing.png' does not exist
|
|
134
|
+
|
|
135
|
+
# 报错:超过上传上限
|
|
136
|
+
$ miaoda file cp ./video.mp4 /videos/
|
|
137
|
+
Error: File size 230 MB exceeds the 100 MB upload limit
|
|
138
|
+
hint: Split the file, or use the web console for large uploads.
|
|
139
|
+
`);
|
|
140
|
+
fileCmd
|
|
141
|
+
.command("rm")
|
|
142
|
+
.summary("删除一个或多个文件")
|
|
143
|
+
.description("删除一个或多个文件。<file> 可传 path(推荐)或 file_name,可混用。\n" +
|
|
144
|
+
"操作不可撤销(不进回收站),TTY 下默认会要求二次确认。")
|
|
145
|
+
.usage("[paths...] [flags]")
|
|
146
|
+
.argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
|
|
147
|
+
.option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
|
|
148
|
+
.option("-y, --yes", "跳过交互确认;非交互场景必加")
|
|
149
|
+
.action(async (paths, opts) => {
|
|
150
|
+
await (0, index_1.handleFileRm)(paths, opts);
|
|
151
|
+
})
|
|
152
|
+
.addHelpText("after", `
|
|
153
|
+
Notes:
|
|
154
|
+
- 删除不可撤销,没有回收站。
|
|
155
|
+
- 批量场景下失败项不影响其他项;全部成功退出码 0,任意一项失败退出码 1。
|
|
156
|
+
- --json 输出每项保留输入顺序,含 status: "ok" | "error" 字段。
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
# 单文件删除(TTY 下需确认)
|
|
160
|
+
$ miaoda file rm /images/brand/1858537546760216.png
|
|
161
|
+
? Delete '/images/brand/1858537546760216.png'? (y/N) y
|
|
162
|
+
✓ Deleted /images/brand/1858537546760216.png
|
|
163
|
+
|
|
164
|
+
# 批量删除(混用 path 与 file_name)
|
|
165
|
+
$ miaoda file rm /images/A.png /images/B.png /docs/C.pdf --yes
|
|
166
|
+
✓ Deleted /images/A.png
|
|
167
|
+
✓ Deleted /images/B.png
|
|
168
|
+
✓ Deleted /docs/C.pdf
|
|
169
|
+
Deleted 3 of 3 files
|
|
170
|
+
|
|
171
|
+
# 部分失败(其他项仍会被删除)
|
|
172
|
+
$ miaoda file rm logo.png missing.png report.pdf --yes
|
|
173
|
+
✓ Deleted logo.png
|
|
174
|
+
✗ missing.png: File does not exist
|
|
175
|
+
✓ Deleted report.pdf
|
|
176
|
+
Deleted 2 of 3 files (1 failed)
|
|
177
|
+
|
|
178
|
+
# 报错:file_name 多匹配
|
|
179
|
+
$ miaoda file rm logo.png --yes
|
|
180
|
+
Error: Multiple files match name 'logo.png' (2 found)
|
|
181
|
+
hint: Use path instead. Run \`miaoda file ls --name logo.png\` to see candidates.
|
|
182
|
+
`);
|
|
183
|
+
fileCmd
|
|
184
|
+
.command("sign")
|
|
185
|
+
.summary("生成公网可访问的临时下载链接")
|
|
186
|
+
.description("生成 signed_url——公网可直接访问的临时下载链接,带过期时间。\n" +
|
|
187
|
+
"仅在需要浏览器打开 / 公网分享时使用;应用代码内引用文件请用 download_url\n" +
|
|
188
|
+
"(来自 cp 或 stat),无需 sign。")
|
|
189
|
+
.usage("<file> [flags]")
|
|
190
|
+
.argument("<file>", "文件的路径或文件名")
|
|
191
|
+
.option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 等单位(默认 1d,最长 30d)")
|
|
192
|
+
.action(async (file, opts) => {
|
|
193
|
+
await (0, index_1.handleFileSign)(file, opts);
|
|
194
|
+
})
|
|
195
|
+
.addHelpText("after", `
|
|
196
|
+
Notes:
|
|
197
|
+
- <file> 可传 path(推荐)或 file_name;重名报 AMBIGUOUS_FILE_NAME。
|
|
198
|
+
- signed_url 会过期,不要持久化到数据库。永久引用请用 download_url。
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
$ miaoda file sign /images/brand/1858537546760216.png
|
|
202
|
+
https://miaoda.feishu.cn/storage/.../1858537546760216.png?token=xxx&expires=86400
|
|
203
|
+
|
|
204
|
+
$ miaoda file sign /images/brand/1858537546760216.png --expires 30d
|
|
205
|
+
https://miaoda.feishu.cn/storage/.../1858537546760216.png?token=xxx&expires=2592000
|
|
206
|
+
|
|
207
|
+
# 报错:过期时间超过上限
|
|
208
|
+
$ miaoda file sign /images/brand/1858537546760216.png --expires 60d
|
|
209
|
+
Error: Expires duration '60d' exceeds the maximum of 30d
|
|
210
|
+
hint: Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerCommands = registerCommands;
|
|
4
4
|
const index_1 = require("../../cli/commands/plugin/index");
|
|
5
|
+
const index_2 = require("../../cli/commands/file/index");
|
|
6
|
+
const index_3 = require("../../cli/commands/db/index");
|
|
5
7
|
function registerCommands(program) {
|
|
6
8
|
(0, index_1.registerPluginCommands)(program);
|
|
9
|
+
(0, index_2.registerFileCommands)(program);
|
|
10
|
+
(0, index_3.registerDbCommands)(program);
|
|
7
11
|
}
|
|
@@ -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
|
});
|