@lark-apaas/miaoda-cli 0.1.1-alpha.a1a5e34 → 0.1.1-alpha.b2bef50
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/client.js +8 -9
- package/dist/api/db/sql-keywords.js +87 -16
- package/dist/api/file/api.js +7 -5
- package/dist/api/file/client.js +1 -5
- package/dist/api/file/parsers.js +1 -5
- package/dist/api/plugin/api.js +8 -3
- package/dist/cli/commands/plugin/index.js +18 -6
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/handlers/db/data.js +11 -3
- package/dist/cli/handlers/db/schema.js +8 -5
- package/dist/cli/handlers/db/sql.js +16 -12
- package/dist/cli/handlers/file/cp.js +22 -11
- package/dist/cli/handlers/file/ls.js +1 -3
- package/dist/cli/handlers/file/rm.js +1 -1
- package/dist/cli/handlers/plugin/plugin-local.js +23 -9
- package/dist/cli/handlers/plugin/plugin.js +21 -7
- package/dist/cli/help.js +5 -2
- package/dist/utils/output.js +5 -4
- package/dist/utils/render.js +44 -29
- package/package.json +8 -1
package/dist/api/db/client.js
CHANGED
|
@@ -27,11 +27,7 @@ function traceHttp(method, url, start, response, err) {
|
|
|
27
27
|
const status = response?.status ?? 0;
|
|
28
28
|
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
29
29
|
if (err !== undefined) {
|
|
30
|
-
const errMsg = err instanceof Error
|
|
31
|
-
? err.message
|
|
32
|
-
: typeof err === "string"
|
|
33
|
-
? err
|
|
34
|
-
: JSON.stringify(err);
|
|
30
|
+
const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
35
31
|
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
36
32
|
return;
|
|
37
33
|
}
|
|
@@ -120,16 +116,19 @@ function extractSqlstate(msg) {
|
|
|
120
116
|
const BIZ_ERR_MAP = new Map(Object.entries({
|
|
121
117
|
// 来源:真实环境探测(dataloom InnerExecuteSQL / InnerGetSchema)
|
|
122
118
|
// k_dl_000001:DB 连接 / QPS 限流 等基础设施层错误
|
|
123
|
-
|
|
119
|
+
k_dl_000001: {
|
|
120
|
+
code: "INTERNAL_DB_ERROR",
|
|
121
|
+
hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试",
|
|
122
|
+
},
|
|
124
123
|
// k_dl_000002:SQL 解析 / 不支持的 SQL 类型(VACUUM、COPY、SET、SHOW 等)
|
|
125
|
-
|
|
124
|
+
k_dl_000002: { code: "SQL_SYNTAX_ERROR" },
|
|
126
125
|
// k_dl_000003:查询命中系统表(pg_tables / pg_user 等)被拒
|
|
127
|
-
|
|
126
|
+
k_dl_000003: { code: "SQL_OPERATION_FORBIDDEN" },
|
|
128
127
|
// k_dl_1300002:PG 执行错误;实际 SQLSTATE 由 extractSqlstate 单独映射
|
|
129
128
|
// 未匹配到 SQLSTATE 时走下面兜底 DB_API_<code>
|
|
130
129
|
// k_dl_1300015:SELECT 结果超过 1000 行硬拦;多行 hint 由 output.ts 的
|
|
131
130
|
// SERVER_ERROR_HINTS 按语义 code 兜底,这里只做 code 改名
|
|
132
|
-
|
|
131
|
+
k_dl_1300015: { code: "RESULT_SET_TOO_LARGE" },
|
|
133
132
|
}));
|
|
134
133
|
/** PG SQLSTATE → CLI code(当前 dataloom 不一定透传,预留未来使用) */
|
|
135
134
|
exports.SQLSTATE_MAP = {
|
|
@@ -21,32 +21,103 @@ exports.SQL_KEYWORDS = void 0;
|
|
|
21
21
|
*/
|
|
22
22
|
exports.SQL_KEYWORDS = [
|
|
23
23
|
// DML 动词
|
|
24
|
-
"SELECT",
|
|
24
|
+
"SELECT",
|
|
25
|
+
"INSERT",
|
|
26
|
+
"UPDATE",
|
|
27
|
+
"DELETE",
|
|
28
|
+
"MERGE",
|
|
25
29
|
// FROM / JOIN 系列
|
|
26
|
-
"FROM",
|
|
30
|
+
"FROM",
|
|
31
|
+
"WHERE",
|
|
32
|
+
"JOIN",
|
|
33
|
+
"LEFT",
|
|
34
|
+
"RIGHT",
|
|
35
|
+
"INNER",
|
|
36
|
+
"OUTER",
|
|
37
|
+
"FULL",
|
|
38
|
+
"CROSS",
|
|
39
|
+
"USING",
|
|
27
40
|
// 聚合 / 排序 / 分页
|
|
28
|
-
"GROUP",
|
|
41
|
+
"GROUP",
|
|
42
|
+
"ORDER",
|
|
43
|
+
"BY",
|
|
44
|
+
"HAVING",
|
|
45
|
+
"LIMIT",
|
|
46
|
+
"OFFSET",
|
|
47
|
+
"FETCH",
|
|
29
48
|
// 集合操作
|
|
30
|
-
"UNION",
|
|
49
|
+
"UNION",
|
|
50
|
+
"INTERSECT",
|
|
51
|
+
"EXCEPT",
|
|
52
|
+
"DISTINCT",
|
|
53
|
+
"ALL",
|
|
31
54
|
// 别名 / 关联
|
|
32
|
-
"AS",
|
|
55
|
+
"AS",
|
|
56
|
+
"ON",
|
|
33
57
|
// 操作符词
|
|
34
|
-
"AND",
|
|
35
|
-
"
|
|
58
|
+
"AND",
|
|
59
|
+
"OR",
|
|
60
|
+
"NOT",
|
|
61
|
+
"IN",
|
|
62
|
+
"IS",
|
|
63
|
+
"NULL",
|
|
64
|
+
"TRUE",
|
|
65
|
+
"FALSE",
|
|
66
|
+
"BETWEEN",
|
|
67
|
+
"LIKE",
|
|
68
|
+
"ILIKE",
|
|
69
|
+
"SIMILAR",
|
|
36
70
|
// DDL
|
|
37
|
-
"CREATE",
|
|
38
|
-
"
|
|
71
|
+
"CREATE",
|
|
72
|
+
"ALTER",
|
|
73
|
+
"DROP",
|
|
74
|
+
"TRUNCATE",
|
|
75
|
+
"RENAME",
|
|
76
|
+
"TABLE",
|
|
77
|
+
"INDEX",
|
|
78
|
+
"VIEW",
|
|
79
|
+
"DATABASE",
|
|
80
|
+
"SCHEMA",
|
|
81
|
+
"COLUMN",
|
|
82
|
+
"CONSTRAINT",
|
|
83
|
+
"SEQUENCE",
|
|
39
84
|
// 约束
|
|
40
|
-
"PRIMARY",
|
|
85
|
+
"PRIMARY",
|
|
86
|
+
"KEY",
|
|
87
|
+
"FOREIGN",
|
|
88
|
+
"REFERENCES",
|
|
89
|
+
"UNIQUE",
|
|
90
|
+
"CHECK",
|
|
91
|
+
"DEFAULT",
|
|
41
92
|
// 写入
|
|
42
|
-
"VALUES",
|
|
93
|
+
"VALUES",
|
|
94
|
+
"SET",
|
|
95
|
+
"RETURNING",
|
|
96
|
+
"INTO",
|
|
43
97
|
// 事务
|
|
44
|
-
"BEGIN",
|
|
98
|
+
"BEGIN",
|
|
99
|
+
"COMMIT",
|
|
100
|
+
"ROLLBACK",
|
|
101
|
+
"SAVEPOINT",
|
|
102
|
+
"TRANSACTION",
|
|
45
103
|
// 控制流 / CTE
|
|
46
|
-
"IF",
|
|
47
|
-
"
|
|
104
|
+
"IF",
|
|
105
|
+
"EXISTS",
|
|
106
|
+
"REPLACE",
|
|
107
|
+
"WITH",
|
|
108
|
+
"RECURSIVE",
|
|
109
|
+
"CASE",
|
|
110
|
+
"WHEN",
|
|
111
|
+
"THEN",
|
|
112
|
+
"ELSE",
|
|
113
|
+
"END",
|
|
48
114
|
// 类型转换 / 时间提取
|
|
49
|
-
"CAST",
|
|
115
|
+
"CAST",
|
|
116
|
+
"EXTRACT",
|
|
50
117
|
// 排序方向
|
|
51
|
-
"ASC",
|
|
118
|
+
"ASC",
|
|
119
|
+
"DESC",
|
|
120
|
+
"NULLS",
|
|
121
|
+
"FIRST",
|
|
122
|
+
"LAST",
|
|
52
123
|
];
|
package/dist/api/file/api.js
CHANGED
|
@@ -209,9 +209,7 @@ async function resolveInputs(opts) {
|
|
|
209
209
|
return [];
|
|
210
210
|
return Promise.all(opts.inputs.map((input) => {
|
|
211
211
|
const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? "path" : "name");
|
|
212
|
-
return kind === "path"
|
|
213
|
-
? resolveByPath(opts.appId, input)
|
|
214
|
-
: resolveByName(opts.appId, input);
|
|
212
|
+
return kind === "path" ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
|
|
215
213
|
}));
|
|
216
214
|
}
|
|
217
215
|
/** path 输入:HEAD 判存在性,path 在 bucket 内 unique 无需反查。 */
|
|
@@ -309,7 +307,9 @@ async function preUpload(appId, req) {
|
|
|
309
307
|
*/
|
|
310
308
|
async function uploadCallback(appId, req) {
|
|
311
309
|
const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
|
|
312
|
-
const body = await (0, client_1.doPost)(url, req, {
|
|
310
|
+
const body = await (0, client_1.doPost)(url, req, {
|
|
311
|
+
errorContext: "upload callback",
|
|
312
|
+
});
|
|
313
313
|
const data = extractEnvelope(body);
|
|
314
314
|
const metadata = data.metadata;
|
|
315
315
|
if (!metadata) {
|
|
@@ -389,7 +389,9 @@ async function uploadFile(opts) {
|
|
|
389
389
|
(0, logger_1.debug)(`upload callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
390
390
|
}
|
|
391
391
|
const path = metadata.filePath
|
|
392
|
-
?
|
|
392
|
+
? metadata.filePath.startsWith("/")
|
|
393
|
+
? metadata.filePath
|
|
394
|
+
: "/" + metadata.filePath
|
|
393
395
|
: (opts.remotePath ?? "/" + opts.fileName);
|
|
394
396
|
const result = {
|
|
395
397
|
// 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
|
package/dist/api/file/client.js
CHANGED
|
@@ -22,11 +22,7 @@ function traceHttp(method, url, start, response, err) {
|
|
|
22
22
|
const status = response?.status ?? 0;
|
|
23
23
|
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
24
24
|
if (err !== undefined) {
|
|
25
|
-
const errMsg = err instanceof Error
|
|
26
|
-
? err.message
|
|
27
|
-
: typeof err === "string"
|
|
28
|
-
? err
|
|
29
|
-
: JSON.stringify(err);
|
|
25
|
+
const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
30
26
|
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
31
27
|
return;
|
|
32
28
|
}
|
package/dist/api/file/parsers.js
CHANGED
|
@@ -16,11 +16,7 @@ function int(value) {
|
|
|
16
16
|
function readSize(meta) {
|
|
17
17
|
if (!meta)
|
|
18
18
|
return 0;
|
|
19
|
-
const v = meta.size ??
|
|
20
|
-
meta.fileSize ??
|
|
21
|
-
meta.file_size ??
|
|
22
|
-
meta.contentLength ??
|
|
23
|
-
0;
|
|
19
|
+
const v = meta.size ?? meta.fileSize ?? meta.file_size ?? meta.contentLength ?? 0;
|
|
24
20
|
return int(v);
|
|
25
21
|
}
|
|
26
22
|
/**
|
package/dist/api/plugin/api.js
CHANGED
|
@@ -38,7 +38,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
38
38
|
const versions = await getPluginVersions([pluginKey], isLatest);
|
|
39
39
|
const pluginVersions = versions[pluginKey];
|
|
40
40
|
if (!pluginVersions || pluginVersions.length === 0) {
|
|
41
|
-
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
41
|
+
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
42
|
+
next_actions: ["检查包名拼写,或确认该插件已在插件市场发布"],
|
|
43
|
+
});
|
|
42
44
|
}
|
|
43
45
|
if (isLatest) {
|
|
44
46
|
return pluginVersions[0];
|
|
@@ -53,7 +55,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
53
55
|
function parsePluginKey(key) {
|
|
54
56
|
const match = /^(@[^/]+)\/(.+)$/.exec(key);
|
|
55
57
|
if (!match) {
|
|
56
|
-
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
58
|
+
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
59
|
+
next_actions: ["插件 key 必须形如 @scope/name"],
|
|
60
|
+
});
|
|
57
61
|
}
|
|
58
62
|
return { scope: match[1], name: match[2] };
|
|
59
63
|
}
|
|
@@ -91,7 +95,8 @@ async function withRetry(operation, description, maxRetries = MAX_RETRIES) {
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
|
-
throw lastError ??
|
|
98
|
+
throw (lastError ??
|
|
99
|
+
new error_1.AppError("INTERNAL_RETRY_EXHAUSTED", `${description} failed after ${String(maxRetries)} retries`, { retryable: true, next_actions: ["检查网络后重试,--verbose 可查看重试日志"] }));
|
|
95
100
|
}
|
|
96
101
|
/** 插件缓存目录 */
|
|
97
102
|
const PLUGIN_CACHE_DIR = "node_modules/.cache/miaoda-cli/plugins";
|
|
@@ -58,7 +58,9 @@ JSON 输出
|
|
|
58
58
|
Error: Invalid plugin name format: bad-name. Expected: @scope/name or @scope/name@version
|
|
59
59
|
hint: 示例:@demo/example-plugin 或 @demo/example-plugin@1.2.3
|
|
60
60
|
`)
|
|
61
|
-
.action(async (names) => {
|
|
61
|
+
.action(async (names) => {
|
|
62
|
+
await (0, index_1.handlePluginInstall)({ names });
|
|
63
|
+
});
|
|
62
64
|
pluginCmd
|
|
63
65
|
.command("update")
|
|
64
66
|
.description("把已安装插件升级到 latest 版本")
|
|
@@ -91,7 +93,9 @@ JSON 输出
|
|
|
91
93
|
$ miaoda plugin update @demo/not-installed --json
|
|
92
94
|
{"updated":[],"skipped":[],"notInstalled":["@demo/not-installed"],"failed":[]}
|
|
93
95
|
`)
|
|
94
|
-
.action(async (names) => {
|
|
96
|
+
.action(async (names) => {
|
|
97
|
+
await (0, index_1.handlePluginUpdate)({ names });
|
|
98
|
+
});
|
|
95
99
|
pluginCmd
|
|
96
100
|
.command("remove")
|
|
97
101
|
.description("从当前项目移除一个已安装的插件")
|
|
@@ -115,7 +119,9 @@ JSON 输出
|
|
|
115
119
|
Error: Plugin @demo/not-installed is not installed
|
|
116
120
|
hint: 运行 miaoda plugin list-packages 查看已安装插件
|
|
117
121
|
`)
|
|
118
|
-
.action((name) => {
|
|
122
|
+
.action((name) => {
|
|
123
|
+
(0, index_1.handlePluginRemove)({ name });
|
|
124
|
+
});
|
|
119
125
|
pluginCmd
|
|
120
126
|
.command("init")
|
|
121
127
|
.description("按 package.json 的 actionPlugins 批量安装所有插件")
|
|
@@ -146,7 +152,9 @@ JSON 输出
|
|
|
146
152
|
Error: package.json not found in current directory
|
|
147
153
|
hint: 在应用项目根目录运行
|
|
148
154
|
`)
|
|
149
|
-
.action(async () => {
|
|
155
|
+
.action(async () => {
|
|
156
|
+
await (0, index_1.handlePluginInit)();
|
|
157
|
+
});
|
|
150
158
|
pluginCmd
|
|
151
159
|
.command("list")
|
|
152
160
|
.description("列出当前项目的 capability 实例(./server/capabilities/*.json)")
|
|
@@ -178,7 +186,9 @@ JSON 输出
|
|
|
178
186
|
Error: server/capabilities directory not found
|
|
179
187
|
hint: 当前目录必须是含 server/capabilities/ 的应用项目
|
|
180
188
|
`)
|
|
181
|
-
.action(async (opts) => {
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
await (0, index_1.handlePluginList)(opts);
|
|
191
|
+
});
|
|
182
192
|
pluginCmd
|
|
183
193
|
.command("list-packages")
|
|
184
194
|
.description("列出 package.json actionPlugins 里已声明的插件包")
|
|
@@ -201,5 +211,7 @@ JSON 输出
|
|
|
201
211
|
Error: package.json not found in current directory
|
|
202
212
|
hint: 在应用项目根目录运行
|
|
203
213
|
`)
|
|
204
|
-
.action(() => {
|
|
214
|
+
.action(() => {
|
|
215
|
+
(0, index_1.handlePluginListPlugins)();
|
|
216
|
+
});
|
|
205
217
|
}
|
|
@@ -22,9 +22,7 @@ function resolveAppId(opts) {
|
|
|
22
22
|
const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
|
|
23
23
|
if (!id) {
|
|
24
24
|
throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
|
|
25
|
-
next_actions: [
|
|
26
|
-
"设置 export MIAODA_APP_ID=<id>",
|
|
27
|
-
],
|
|
25
|
+
next_actions: ["设置 export MIAODA_APP_ID=<id>"],
|
|
28
26
|
});
|
|
29
27
|
}
|
|
30
28
|
return id;
|
|
@@ -56,7 +56,9 @@ async function handleDbDataImport(file, opts) {
|
|
|
56
56
|
catch (err) {
|
|
57
57
|
const code = err.code;
|
|
58
58
|
if (code === "ENOENT") {
|
|
59
|
-
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
59
|
+
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
60
|
+
next_actions: ["Check the file path."],
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
throw err;
|
|
62
64
|
}
|
|
@@ -103,7 +105,9 @@ async function handleDbDataExport(table, opts) {
|
|
|
103
105
|
if (!opts.force) {
|
|
104
106
|
try {
|
|
105
107
|
await fs.access(outputPath);
|
|
106
|
-
throw new error_1.AppError("FILE_ALREADY_EXISTS", `Output file '${outputPath}' already exists`, {
|
|
108
|
+
throw new error_1.AppError("FILE_ALREADY_EXISTS", `Output file '${outputPath}' already exists`, {
|
|
109
|
+
next_actions: ["Use -f to specify a different path, or --force to overwrite."],
|
|
110
|
+
});
|
|
107
111
|
}
|
|
108
112
|
catch (err) {
|
|
109
113
|
if (err instanceof error_1.AppError)
|
|
@@ -119,7 +123,11 @@ async function handleDbDataExport(table, opts) {
|
|
|
119
123
|
limit,
|
|
120
124
|
});
|
|
121
125
|
if (result.body.length > MAX_SIZE_BYTES) {
|
|
122
|
-
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
126
|
+
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
127
|
+
next_actions: [
|
|
128
|
+
`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`,
|
|
129
|
+
],
|
|
130
|
+
});
|
|
123
131
|
}
|
|
124
132
|
await fs.writeFile(outputPath, result.body);
|
|
125
133
|
// 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
|
|
@@ -45,7 +45,12 @@ const index_1 = require("../../../api/db/index");
|
|
|
45
45
|
// ── schema list ──
|
|
46
46
|
async function handleDbSchemaList(opts) {
|
|
47
47
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
48
|
-
const resp = await api.db.getSchema({
|
|
48
|
+
const resp = await api.db.getSchema({
|
|
49
|
+
appId,
|
|
50
|
+
format: "schema",
|
|
51
|
+
includeStats: true,
|
|
52
|
+
dbBranch: opts.env,
|
|
53
|
+
});
|
|
49
54
|
const tables = (0, index_1.flattenSchemaList)(resp.schema);
|
|
50
55
|
if ((0, output_1.isJsonMode)()) {
|
|
51
56
|
(0, output_1.emit)({ data: tables });
|
|
@@ -69,7 +74,7 @@ async function handleDbSchemaList(opts) {
|
|
|
69
74
|
t.name,
|
|
70
75
|
t.description ?? "—",
|
|
71
76
|
t.estimated_row_count === null ? "—" : String(t.estimated_row_count),
|
|
72
|
-
t.size_bytes === null ? "—" :
|
|
77
|
+
t.size_bytes === null ? "—" : tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes),
|
|
73
78
|
String(t.columns),
|
|
74
79
|
]);
|
|
75
80
|
(0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
|
|
@@ -109,9 +114,7 @@ async function handleDbSchemaGet(table, opts) {
|
|
|
109
114
|
const detail = (0, index_1.pickTableDetail)(resp.schema, table);
|
|
110
115
|
if (!detail) {
|
|
111
116
|
throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
|
|
112
|
-
next_actions: [
|
|
113
|
-
`Did you mean another table? Run "miaoda db schema list" to see all tables.`,
|
|
114
|
-
],
|
|
117
|
+
next_actions: [`Did you mean another table? Run "miaoda db schema list" to see all tables.`],
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
120
|
if ((0, output_1.isJsonMode)()) {
|
|
@@ -309,7 +309,10 @@ function parseJsonFields() {
|
|
|
309
309
|
const v = (0, config_1.getConfig)().json;
|
|
310
310
|
if (typeof v !== "string" || v === "")
|
|
311
311
|
return null;
|
|
312
|
-
return v
|
|
312
|
+
return v
|
|
313
|
+
.split(",")
|
|
314
|
+
.map((s) => s.trim())
|
|
315
|
+
.filter((s) => s.length > 0);
|
|
313
316
|
}
|
|
314
317
|
/** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
|
|
315
318
|
function renderMultiPretty(results) {
|
|
@@ -349,11 +352,16 @@ function renderMultiPretty(results) {
|
|
|
349
352
|
}
|
|
350
353
|
function dmlVerb(type) {
|
|
351
354
|
switch (type) {
|
|
352
|
-
case "INSERT":
|
|
353
|
-
|
|
354
|
-
case "
|
|
355
|
-
|
|
356
|
-
case "
|
|
355
|
+
case "INSERT":
|
|
356
|
+
return "inserted";
|
|
357
|
+
case "UPDATE":
|
|
358
|
+
return "updated";
|
|
359
|
+
case "DELETE":
|
|
360
|
+
return "deleted";
|
|
361
|
+
case "MERGE":
|
|
362
|
+
return "merged";
|
|
363
|
+
case "DML":
|
|
364
|
+
return "affected"; // 未识别子类的兜底
|
|
357
365
|
}
|
|
358
366
|
}
|
|
359
367
|
/** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
|
|
@@ -500,9 +508,7 @@ function enrichColumnNotExist(err, colMatch, tableMap) {
|
|
|
500
508
|
// TS RegExpMatchArray 数字索引类型 = string,但 optional capture group `(...)?`
|
|
501
509
|
// 没匹配时运行时返 undefined;as cast 把这个事实告诉类型系统。
|
|
502
510
|
const relation = colMatch[2];
|
|
503
|
-
const relationCols = relation !== undefined
|
|
504
|
-
? tableMap[relation]
|
|
505
|
-
: undefined;
|
|
511
|
+
const relationCols = relation !== undefined ? tableMap[relation] : undefined;
|
|
506
512
|
if (relation !== undefined && relationCols !== undefined) {
|
|
507
513
|
// 指定了表 → 在该表的列里找
|
|
508
514
|
const guess = (0, fuzzy_match_1.suggest)(colName, relationCols);
|
|
@@ -609,9 +615,7 @@ function inferRolledBack(completed) {
|
|
|
609
615
|
* dollar-quoted($$..$$)等高级形态——估错 ±1 不影响 hint 可读性。
|
|
610
616
|
*/
|
|
611
617
|
function countStatements(sql) {
|
|
612
|
-
const stripped = sql
|
|
613
|
-
.replace(/'(?:''|[^'])*'/g, "''")
|
|
614
|
-
.replace(/"(?:""|[^"])*"/g, '""');
|
|
618
|
+
const stripped = sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""');
|
|
615
619
|
return stripped.split(/;+/).filter((s) => s.trim().length > 0).length;
|
|
616
620
|
}
|
|
617
621
|
/**
|
|
@@ -120,19 +120,30 @@ async function handleUpload(appId, localRaw, remoteRaw, rename) {
|
|
|
120
120
|
const stat = node_fs_1.default.statSync(localPath);
|
|
121
121
|
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
122
122
|
throw new error_1.AppError("FILE_SIZE_EXCEEDED", `File size ${(0, render_1.formatSize)(stat.size)} exceeds the 100 MB upload limit`, {
|
|
123
|
-
next_actions: [
|
|
124
|
-
"Split the file, or use the web console for large uploads.",
|
|
125
|
-
],
|
|
123
|
+
next_actions: ["Split the file, or use the web console for large uploads."],
|
|
126
124
|
});
|
|
127
125
|
}
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
126
|
+
// PRD:path 末段**始终**由平台生成 16 位 ID 确保唯一,不受 dst 形态或
|
|
127
|
+
// --rename 影响;用户指定的 file_name 仅作为显示名(同目录下允许重名)。
|
|
128
|
+
// 因此 CLI 端把 dst 拆成"目录前缀 + file_name"两段:
|
|
129
|
+
// - dst 以 / 结尾或为空 → 整段当目录前缀,file_name 用 --rename 或本地 basename
|
|
130
|
+
// 例:cp ./logo.png /imgs/ → path=/imgs/<GID>.png, file_name=logo.png
|
|
131
|
+
// - dst 不以 / 结尾 → 末段当 file_name,前段(含尾 /)当目录前缀
|
|
132
|
+
// 例:cp ./1.jpg /post-covers/cover.jpg → path=/post-covers/<GID>.jpg, file_name=cover.jpg
|
|
133
|
+
// 这样无论用户怎么写 dst,都走服务端目录前缀模式(PreUpload `filePath`
|
|
134
|
+
// 末尾带 /),保证 path 全局唯一性,不会因用户指定了完整路径而退化成
|
|
135
|
+
// 完整对象 key 模式。--rename 始终覆盖 file_name 推断结果(PRD 优先级)。
|
|
136
|
+
let fileName;
|
|
137
|
+
let remotePath;
|
|
138
|
+
if (remoteRaw === "" || remoteRaw.endsWith("/")) {
|
|
139
|
+
fileName = rename ?? node_path_1.default.basename(localPath);
|
|
140
|
+
remotePath = remoteRaw;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const lastSlash = remoteRaw.lastIndexOf("/");
|
|
144
|
+
fileName = rename ?? remoteRaw.slice(lastSlash + 1);
|
|
145
|
+
remotePath = remoteRaw.slice(0, lastSlash + 1);
|
|
146
|
+
}
|
|
136
147
|
const contentType = detectMime(localPath);
|
|
137
148
|
const result = await api.file.uploadFile({
|
|
138
149
|
appId,
|
|
@@ -101,9 +101,7 @@ async function handleFileLs(opts) {
|
|
|
101
101
|
info.type,
|
|
102
102
|
(0, render_1.formatTime)(info.uploaded_at, tty),
|
|
103
103
|
]);
|
|
104
|
-
const table = tty
|
|
105
|
-
? (0, render_1.renderAlignedTable)(headers, rows)
|
|
106
|
-
: (0, render_1.renderTsv)(headers, rows);
|
|
104
|
+
const table = tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows);
|
|
107
105
|
const hint = result.has_more && result.next_cursor
|
|
108
106
|
? `\n— ${String(result.items.length)} results. Next: --cursor ${result.next_cursor}`
|
|
109
107
|
: "";
|
|
@@ -170,7 +170,7 @@ async function handleFileRm(paths, opts) {
|
|
|
170
170
|
results.push({
|
|
171
171
|
status: "ok",
|
|
172
172
|
input: entry?.input ?? p,
|
|
173
|
-
file_name: entry?.file_name ??
|
|
173
|
+
file_name: entry?.file_name ?? p.split("/").pop() ?? p,
|
|
174
174
|
path: p,
|
|
175
175
|
});
|
|
176
176
|
}
|
|
@@ -82,7 +82,9 @@ function parsePluginName(input) {
|
|
|
82
82
|
function readPackageJson() {
|
|
83
83
|
const pkgPath = getPackageJsonPath();
|
|
84
84
|
if (!node_fs_1.default.existsSync(pkgPath)) {
|
|
85
|
-
throw new error_1.AppError("PKG_JSON_NOT_FOUND", "package.json not found in current directory", {
|
|
85
|
+
throw new error_1.AppError("PKG_JSON_NOT_FOUND", "package.json not found in current directory", {
|
|
86
|
+
next_actions: ["在应用项目根目录运行"],
|
|
87
|
+
});
|
|
86
88
|
}
|
|
87
89
|
const content = node_fs_1.default.readFileSync(pkgPath, "utf-8");
|
|
88
90
|
return JSON.parse(content);
|
|
@@ -189,9 +191,14 @@ function installMissingDeps(deps) {
|
|
|
189
191
|
if (deps.length === 0)
|
|
190
192
|
return;
|
|
191
193
|
(0, logger_1.log)("plugin", `Installing missing dependencies: ${deps.join(", ")}`);
|
|
192
|
-
const result = (0, node_child_process_1.spawnSync)("npm", ["install", ...deps, "--no-save", "--no-package-lock"], {
|
|
194
|
+
const result = (0, node_child_process_1.spawnSync)("npm", ["install", ...deps, "--no-save", "--no-package-lock"], {
|
|
195
|
+
cwd: getProjectRoot(),
|
|
196
|
+
stdio: "inherit",
|
|
197
|
+
});
|
|
193
198
|
if (result.error) {
|
|
194
|
-
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
|
|
199
|
+
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
|
|
200
|
+
next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"],
|
|
201
|
+
});
|
|
195
202
|
}
|
|
196
203
|
if (result.status !== 0) {
|
|
197
204
|
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed with exit code ${String(result.status)}`, { next_actions: ["检查上方 npm 输出日志定位具体错误"] });
|
|
@@ -200,7 +207,9 @@ function installMissingDeps(deps) {
|
|
|
200
207
|
function npmInstall(tgzPath) {
|
|
201
208
|
const result = (0, node_child_process_1.spawnSync)("npm", ["install", tgzPath, "--no-save", "--no-package-lock", "--ignore-scripts"], { cwd: getProjectRoot(), stdio: "inherit" });
|
|
202
209
|
if (result.error) {
|
|
203
|
-
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
|
|
210
|
+
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
|
|
211
|
+
next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"],
|
|
212
|
+
});
|
|
204
213
|
}
|
|
205
214
|
if (result.status !== 0) {
|
|
206
215
|
throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed with exit code ${String(result.status)}`, { next_actions: ["检查上方 npm 输出日志定位具体错误"] });
|
|
@@ -232,7 +241,9 @@ function listCapabilityIds() {
|
|
|
232
241
|
function readCapability(id) {
|
|
233
242
|
const filePath = node_path_1.default.join(getCapabilitiesDir(), `${id}.json`);
|
|
234
243
|
if (!node_fs_1.default.existsSync(filePath)) {
|
|
235
|
-
throw new error_1.AppError("CAPABILITY_NOT_FOUND", `Capability not found: ${id}`, {
|
|
244
|
+
throw new error_1.AppError("CAPABILITY_NOT_FOUND", `Capability not found: ${id}`, {
|
|
245
|
+
next_actions: ["运行 miaoda plugin list 查看所有可用 capability id"],
|
|
246
|
+
});
|
|
236
247
|
}
|
|
237
248
|
try {
|
|
238
249
|
const content = node_fs_1.default.readFileSync(filePath, "utf-8");
|
|
@@ -240,7 +251,9 @@ function readCapability(id) {
|
|
|
240
251
|
}
|
|
241
252
|
catch (error) {
|
|
242
253
|
if (error instanceof SyntaxError) {
|
|
243
|
-
throw new error_1.AppError("INVALID_JSON", `Invalid JSON in capability file: ${id}.json`, {
|
|
254
|
+
throw new error_1.AppError("INVALID_JSON", `Invalid JSON in capability file: ${id}.json`, {
|
|
255
|
+
next_actions: [`检查 server/capabilities/${id}.json 的 JSON 语法`],
|
|
256
|
+
});
|
|
244
257
|
}
|
|
245
258
|
throw error;
|
|
246
259
|
}
|
|
@@ -303,7 +316,9 @@ async function loadPlugin(pluginKey) {
|
|
|
303
316
|
}
|
|
304
317
|
catch (error) {
|
|
305
318
|
if (error.code === "MODULE_NOT_FOUND") {
|
|
306
|
-
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not installed: ${pluginKey}`, {
|
|
319
|
+
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not installed: ${pluginKey}`, {
|
|
320
|
+
next_actions: [`运行 miaoda plugin install ${pluginKey}`],
|
|
321
|
+
});
|
|
307
322
|
}
|
|
308
323
|
throw new error_1.AppError("INTERNAL_PLUGIN_LOAD_FAILED", `Failed to load plugin ${pluginKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
324
|
}
|
|
@@ -314,8 +329,7 @@ async function hydrateCapability(capability) {
|
|
|
314
329
|
if (manifest.actions.length === 0) {
|
|
315
330
|
throw new error_1.AppError("INTERNAL_PLUGIN_LOAD_FAILED", `Plugin ${capability.pluginKey} has no actions defined`);
|
|
316
331
|
}
|
|
317
|
-
const hasDynamic = manifest.actions.some((action) => isDynamicSchema(action.inputSchema) ||
|
|
318
|
-
isDynamicSchema(action.outputSchema));
|
|
332
|
+
const hasDynamic = manifest.actions.some((action) => isDynamicSchema(action.inputSchema) || isDynamicSchema(action.outputSchema));
|
|
319
333
|
let pluginInstance = null;
|
|
320
334
|
if (hasDynamic) {
|
|
321
335
|
const plugin = await loadPlugin(capability.pluginKey);
|
|
@@ -44,7 +44,9 @@ const output_1 = require("../../../utils/output");
|
|
|
44
44
|
const error_1 = require("../../../utils/error");
|
|
45
45
|
const logger_1 = require("../../../utils/logger");
|
|
46
46
|
const plugin_local_1 = require("./plugin-local");
|
|
47
|
-
const log = (msg) => {
|
|
47
|
+
const log = (msg) => {
|
|
48
|
+
(0, logger_1.log)("plugin", msg);
|
|
49
|
+
};
|
|
48
50
|
// ── Install ──
|
|
49
51
|
function syncActionPluginsRecord(name, version) {
|
|
50
52
|
const plugins = (0, plugin_local_1.readActionPlugins)();
|
|
@@ -63,7 +65,9 @@ async function installOne(nameWithVersion) {
|
|
|
63
65
|
if (actualVersion === requestedVersion) {
|
|
64
66
|
log(`${name}@${requestedVersion} already installed`);
|
|
65
67
|
syncActionPluginsRecord(name, actualVersion);
|
|
66
|
-
api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
|
|
68
|
+
api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
|
|
69
|
+
/* fire-and-forget */
|
|
70
|
+
});
|
|
67
71
|
return { name, version: actualVersion, success: true, skipped: true };
|
|
68
72
|
}
|
|
69
73
|
}
|
|
@@ -74,7 +78,9 @@ async function installOne(nameWithVersion) {
|
|
|
74
78
|
if (actualVersion === targetVersion) {
|
|
75
79
|
log(`${name} already up to date (${actualVersion})`);
|
|
76
80
|
syncActionPluginsRecord(name, actualVersion);
|
|
77
|
-
api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
|
|
81
|
+
api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
|
|
82
|
+
/* fire-and-forget */
|
|
83
|
+
});
|
|
78
84
|
return { name, version: actualVersion, success: true, skipped: true };
|
|
79
85
|
}
|
|
80
86
|
log(`Found newer version: ${targetVersion} (installed: ${actualVersion ?? "none"})`);
|
|
@@ -106,8 +112,12 @@ async function installOne(nameWithVersion) {
|
|
|
106
112
|
(0, plugin_local_1.writeActionPlugins)(plugins);
|
|
107
113
|
const source = fromCache ? "from cache" : "downloaded";
|
|
108
114
|
log(`Installed ${name}@${installedVersion} (${source})`);
|
|
109
|
-
api.plugin.reportInstallEvent(name, installedVersion).catch(() => {
|
|
110
|
-
|
|
115
|
+
api.plugin.reportInstallEvent(name, installedVersion).catch(() => {
|
|
116
|
+
/* fire-and-forget */
|
|
117
|
+
});
|
|
118
|
+
api.plugin.reportCreateInstanceEvent(name, installedVersion).catch(() => {
|
|
119
|
+
/* fire-and-forget */
|
|
120
|
+
});
|
|
111
121
|
return { name, version: installedVersion, success: true };
|
|
112
122
|
}
|
|
113
123
|
catch (error) {
|
|
@@ -193,7 +203,9 @@ async function handlePluginUpdate(opts) {
|
|
|
193
203
|
function handlePluginRemove(opts) {
|
|
194
204
|
const { name } = (0, plugin_local_1.parsePluginName)(opts.name);
|
|
195
205
|
if (!(0, plugin_local_1.isPluginInstalled)(name)) {
|
|
196
|
-
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin ${name} is not installed`, {
|
|
206
|
+
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin ${name} is not installed`, {
|
|
207
|
+
next_actions: ["运行 miaoda plugin list-packages 查看已安装插件"],
|
|
208
|
+
});
|
|
197
209
|
}
|
|
198
210
|
(0, plugin_local_1.removePluginDirectory)(name);
|
|
199
211
|
const plugins = (0, plugin_local_1.readActionPlugins)();
|
|
@@ -263,7 +275,9 @@ async function handlePluginInit() {
|
|
|
263
275
|
// ── List (capability configs) ──
|
|
264
276
|
async function handlePluginList(opts) {
|
|
265
277
|
if (!(0, plugin_local_1.capabilitiesDirExists)()) {
|
|
266
|
-
throw new error_1.AppError("CAPABILITIES_DIR_NOT_FOUND", "server/capabilities directory not found", {
|
|
278
|
+
throw new error_1.AppError("CAPABILITIES_DIR_NOT_FOUND", "server/capabilities directory not found", {
|
|
279
|
+
next_actions: ["当前目录必须是含 server/capabilities/ 的应用项目"],
|
|
280
|
+
});
|
|
267
281
|
}
|
|
268
282
|
if (opts.id) {
|
|
269
283
|
const capability = (0, plugin_local_1.readCapability)(opts.id);
|
package/dist/cli/help.js
CHANGED
|
@@ -115,7 +115,9 @@ class MiaodaHelp extends commander_1.Help {
|
|
|
115
115
|
out.push("Usage:", ` ${helper.commandUsage(cmd)}`, "");
|
|
116
116
|
// 3. Commands(仅父级命令组有,spec 要求 Commands 在 Flags 前)
|
|
117
117
|
// spec 不展示 Arguments 段,参数说明放在 description 文本里
|
|
118
|
-
const subs = helper
|
|
118
|
+
const subs = helper
|
|
119
|
+
.visibleCommands(cmd)
|
|
120
|
+
.map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
|
|
119
121
|
if (subs.length) {
|
|
120
122
|
out.push("Commands:", formatList(subs), "");
|
|
121
123
|
}
|
|
@@ -125,7 +127,8 @@ class MiaodaHelp extends commander_1.Help {
|
|
|
125
127
|
// - `-h, --help` 永远不放 Flags 段,统一放 Global Flags(spec 约定)
|
|
126
128
|
const isParent = subs.length > 0;
|
|
127
129
|
if (!isRoot && !isParent) {
|
|
128
|
-
const opts = helper
|
|
130
|
+
const opts = helper
|
|
131
|
+
.visibleOptions(cmd)
|
|
129
132
|
.filter((o) => !isHelpOption(o))
|
|
130
133
|
.map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
|
|
131
134
|
if (opts.length) {
|
package/dist/utils/output.js
CHANGED
|
@@ -23,7 +23,7 @@ const SERVER_ERROR_HINTS = {
|
|
|
23
23
|
// SELECT 结果集超过 1000 行硬拦:spec 多行引导。
|
|
24
24
|
// key 用 BIZ_ERR_MAP 映射后的语义 code(不是原始服务端 k_dl_xxx),保持
|
|
25
25
|
// 与 help / 文档里宣传的 RESULT_SET_TOO_LARGE 一致。
|
|
26
|
-
|
|
26
|
+
RESULT_SET_TOO_LARGE: [
|
|
27
27
|
"Add `LIMIT <n>` to your SQL to narrow the result.",
|
|
28
28
|
"For counts, use `SELECT count(*) FROM ...`; for aggregation, use GROUP BY with filters.",
|
|
29
29
|
],
|
|
@@ -90,9 +90,10 @@ function emitError(err) {
|
|
|
90
90
|
if (typeof info.statement_index === "number") {
|
|
91
91
|
const k = info.statement_index + 1;
|
|
92
92
|
const n = info.total_statements;
|
|
93
|
-
errorLine +=
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
errorLine +=
|
|
94
|
+
typeof n === "number" && n > 0
|
|
95
|
+
? ` (at statement ${String(k)} of ${String(n)})`
|
|
96
|
+
: ` (at statement ${String(k)})`;
|
|
96
97
|
}
|
|
97
98
|
process.stderr.write(errorLine + "\n");
|
|
98
99
|
// 多行 hint:第一行带 "hint:" 标签,后续行用 8 空格缩进对齐 " hint: " 之后的列。
|
package/dist/utils/render.js
CHANGED
|
@@ -81,20 +81,21 @@ const ANSI_SGR_RE = /\[[0-9;]*m/g;
|
|
|
81
81
|
* 不实现合字 / 零宽字符(ZWJ / 变体选择符)等极端情况,CLI 表格场景够用。
|
|
82
82
|
*/
|
|
83
83
|
function charWidth(cp) {
|
|
84
|
-
if ((cp >= 0x1100 && cp <=
|
|
85
|
-
cp === 0x2329 ||
|
|
86
|
-
|
|
87
|
-
(cp >=
|
|
88
|
-
(cp >=
|
|
89
|
-
(cp >=
|
|
90
|
-
(cp >=
|
|
91
|
-
(cp >=
|
|
92
|
-
(cp >=
|
|
93
|
-
(cp >=
|
|
94
|
-
(cp >=
|
|
95
|
-
(cp >=
|
|
96
|
-
(cp >=
|
|
97
|
-
(cp >=
|
|
84
|
+
if ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
85
|
+
cp === 0x2329 ||
|
|
86
|
+
cp === 0x232a ||
|
|
87
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals / Punctuation
|
|
88
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana / Katakana / CJK Symbols
|
|
89
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
|
|
90
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
|
|
91
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
92
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
|
|
93
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compat Ideographs
|
|
94
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK Compat Forms
|
|
95
|
+
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
|
|
96
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
97
|
+
(cp >= 0x20000 && cp <= 0x2fffd) || // CJK Ext B-F
|
|
98
|
+
(cp >= 0x30000 && cp <= 0x3fffd) // CJK Ext G-H
|
|
98
99
|
) {
|
|
99
100
|
return 2;
|
|
100
101
|
}
|
|
@@ -126,9 +127,15 @@ function renderAlignedTable(headers, rows) {
|
|
|
126
127
|
return w;
|
|
127
128
|
});
|
|
128
129
|
const lines = [];
|
|
129
|
-
lines.push(headers
|
|
130
|
+
lines.push(headers
|
|
131
|
+
.map((h, i) => colors_1.c.header(padVisibleEnd(h, colWidths[i])))
|
|
132
|
+
.join(" ")
|
|
133
|
+
.trimEnd());
|
|
130
134
|
for (const row of rows) {
|
|
131
|
-
lines.push(row
|
|
135
|
+
lines.push(row
|
|
136
|
+
.map((cell, i) => padVisibleEnd(cell || "", colWidths[i]))
|
|
137
|
+
.join(" ")
|
|
138
|
+
.trimEnd());
|
|
132
139
|
}
|
|
133
140
|
return lines.join("\n");
|
|
134
141
|
}
|
|
@@ -149,9 +156,7 @@ function renderKeyValue(pairs, isTty) {
|
|
|
149
156
|
return pairs.map(([k, v]) => `${k}\t${v}`).join("\n");
|
|
150
157
|
}
|
|
151
158
|
const keyWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
152
|
-
return pairs
|
|
153
|
-
.map(([k, v]) => `${colors_1.c.header(k.padStart(keyWidth))}: ${v}`)
|
|
154
|
-
.join("\n");
|
|
159
|
+
return pairs.map(([k, v]) => `${colors_1.c.header(k.padStart(keyWidth))}: ${v}`).join("\n");
|
|
155
160
|
}
|
|
156
161
|
/** 通用 isTTY 判定(stdout 是否交互终端)。Node 运行时 isTTY 为 true 或 undefined;TS 类型上 tty.WriteStream 定义为固定 true,绕开做运行时判断。 */
|
|
157
162
|
function isStdoutTty() {
|
|
@@ -167,11 +172,16 @@ function parseDuration(input) {
|
|
|
167
172
|
const n = Number(m[1]);
|
|
168
173
|
const unit = m[2] || "s";
|
|
169
174
|
switch (unit) {
|
|
170
|
-
case "s":
|
|
171
|
-
|
|
172
|
-
case "
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
case "s":
|
|
176
|
+
return n;
|
|
177
|
+
case "m":
|
|
178
|
+
return n * 60;
|
|
179
|
+
case "h":
|
|
180
|
+
return n * 3600;
|
|
181
|
+
case "d":
|
|
182
|
+
return n * 86400;
|
|
183
|
+
default:
|
|
184
|
+
return n;
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
/** 解析 size 字符串 `1MB` / `500KB` / `1GB` → 字节。 */
|
|
@@ -183,10 +193,15 @@ function parseSize(input) {
|
|
|
183
193
|
const n = Number(m[1]);
|
|
184
194
|
const unit = (m[2] || "B").toUpperCase();
|
|
185
195
|
switch (unit) {
|
|
186
|
-
case "B":
|
|
187
|
-
|
|
188
|
-
case "
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
case "B":
|
|
197
|
+
return Math.round(n);
|
|
198
|
+
case "KB":
|
|
199
|
+
return Math.round(n * 1024);
|
|
200
|
+
case "MB":
|
|
201
|
+
return Math.round(n * 1024 * 1024);
|
|
202
|
+
case "GB":
|
|
203
|
+
return Math.round(n * 1024 * 1024 * 1024);
|
|
204
|
+
default:
|
|
205
|
+
return Math.round(n);
|
|
191
206
|
}
|
|
192
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.1-alpha.
|
|
3
|
+
"version": "0.1.1-alpha.b2bef50",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,12 @@
|
|
|
35
35
|
"@typescript-eslint/parser": "^8.58.2",
|
|
36
36
|
"@vitest/coverage-v8": "^4.1.4",
|
|
37
37
|
"eslint": "^9.25.1",
|
|
38
|
+
"eslint-config-prettier": "^10.1.8",
|
|
39
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
40
|
+
"eslint-plugin-boundaries": "^6.0.2",
|
|
41
|
+
"eslint-plugin-import": "^2.32.0",
|
|
38
42
|
"husky": "^9.1.7",
|
|
43
|
+
"prettier": "^3.8.3",
|
|
39
44
|
"tsc-alias": "^1.8.11",
|
|
40
45
|
"tsx": "^4.19.4",
|
|
41
46
|
"typescript": "^5.8.3",
|
|
@@ -46,6 +51,8 @@
|
|
|
46
51
|
"build": "bash scripts/build.sh",
|
|
47
52
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
48
53
|
"lint": "eslint src/ --max-warnings 0",
|
|
54
|
+
"format": "prettier --write src/",
|
|
55
|
+
"format:check": "prettier --check src/",
|
|
49
56
|
"test": "vitest run",
|
|
50
57
|
"test:watch": "vitest",
|
|
51
58
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|