@lark-apaas/miaoda-cli 0.1.1-alpha.e70b415 → 0.1.1-alpha.eccb0b7

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.
@@ -96,15 +96,55 @@ async function execSql(opts) {
96
96
  const body = (await response.json());
97
97
  try {
98
98
  const data = (0, client_1.extractData)(body);
99
- return data.results ?? [];
99
+ const results = data.results ?? [];
100
+ // 新协议(dataloom 端 InnerAdminExecuteSQL 错误统一走 success envelope):
101
+ // results 末尾追加一条 SqlType="ERROR" 的哨兵,data 字段是 {code,message} JSON。
102
+ // 这样网关在错误路径不会吞业务字段,CLI 拿到完整 partial_results + statement_index。
103
+ const last = results.at(-1);
104
+ if (last?.sqlType === "ERROR") {
105
+ const appErr = parseSqlErrorSentinel(last.data);
106
+ appErr.partial_results = results.slice(0, -1);
107
+ const stmtIdx = data.errorStatementIndex;
108
+ if (typeof stmtIdx === "number")
109
+ appErr.statement_index = stmtIdx;
110
+ throw appErr;
111
+ }
112
+ return results;
100
113
  }
101
114
  catch (appErr) {
102
- // HTTP 2xx 但 envelope status_code != 0 的多语句失败路径:同样挂 partial_results
103
- if (appErr instanceof error_1.AppError)
115
+ // 旧协议兼容(dataloom 服务端未部署新协议时):HTTP 2xx 但 envelope status_code != 0
116
+ // mapDbHttpError AppError,partial_results 已被网关吞,仅保留 code+message。
117
+ //
118
+ // 新协议下上面的 try 块里已经手动 slice 设置了 partial_results(剥离了 ERROR 哨兵),
119
+ // 这里必须跳过 attach,否则会被 body.data.results 完整数组(含哨兵)覆盖回去。
120
+ if (appErr instanceof error_1.AppError && appErr.partial_results === undefined) {
104
121
  attachSqlPartialResults(body, appErr);
122
+ }
105
123
  throw appErr;
106
124
  }
107
125
  }
126
+ /**
127
+ * 解析 InnerAdminExecuteSQL 错误哨兵(results 末尾那条 SqlType="ERROR" 项)的 data 字段。
128
+ * 服务端约定:data 是 `{"code":"k_dl_xxx","message":"..."}` 形态的 JSON 字符串。
129
+ *
130
+ * 拿到 code+message 后复用 mapDataloomBizError 走与旧协议(ensureInnerSuccess)
131
+ * 完全一致的映射规则——k_dl_1300015 → RESULT_SET_TOO_LARGE、SQLSTATE 透传等
132
+ * hint / 语义 code 重写不能丢。否则新协议下 SERVER_ERROR_HINTS 会因为 code key
133
+ * 不匹配而 miss,CLI 用户看不到 LIMIT 引导这种关键 hint。
134
+ *
135
+ * 解析失败则降级为 INTERNAL_DB_ERROR + 原始字符串,避免哨兵格式异常时整条调用炸掉。
136
+ */
137
+ function parseSqlErrorSentinel(payload) {
138
+ try {
139
+ const parsed = JSON.parse(payload);
140
+ const code = typeof parsed.code === "string" && parsed.code !== "" ? parsed.code : "INTERNAL_DB_ERROR";
141
+ const message = typeof parsed.message === "string" ? parsed.message : payload;
142
+ return (0, client_1.mapDataloomBizError)(code, message);
143
+ }
144
+ catch {
145
+ return new error_1.AppError("INTERNAL_DB_ERROR", payload);
146
+ }
147
+ }
108
148
  // ── db schema → InnerGetSchema ──
109
149
  /**
110
150
  * 查询 schema(admin-inner)。
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SQLSTATE_MAP = void 0;
4
4
  exports.traceHttp = traceHttp;
5
5
  exports.ensureInnerSuccess = ensureInnerSuccess;
6
+ exports.mapDataloomBizError = mapDataloomBizError;
6
7
  exports.extractData = extractData;
7
8
  exports.buildInnerUrl = buildInnerUrl;
8
9
  const error_1 = require("../../utils/error");
@@ -26,11 +27,7 @@ function traceHttp(method, url, start, response, err) {
26
27
  const status = response?.status ?? 0;
27
28
  const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
28
29
  if (err !== undefined) {
29
- const errMsg = err instanceof Error
30
- ? err.message
31
- : typeof err === "string"
32
- ? err
33
- : JSON.stringify(err);
30
+ const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
34
31
  (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
35
32
  return;
36
33
  }
@@ -54,40 +51,48 @@ function ensureInnerSuccess(body) {
54
51
  const code = body.status_code ?? body.ErrorCode ?? "0";
55
52
  if (code === "0" || code === "")
56
53
  return;
57
- const message = stripPgPrefix(body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
54
+ const appErr = mapDataloomBizError(code, body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
58
55
  // PRD 多语句失败:后端在 envelope 顶层透出 errorStatementIndex(从 0 起计),
59
- // 单语句 / 单元执行不会带这个字段。下面的 AppError 都把它带上,让最终
60
- // CLI JSON envelope 写到 error.statement_index。
61
- const stmtIdx = typeof body.errorStatementIndex === "number" ? body.errorStatementIndex : undefined;
62
- // k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
56
+ // 单语句 / 单元执行不会带这个字段。挂到 AppError 上让 emitError 落到错误信封
57
+ // statement_index 字段。
58
+ if (typeof body.errorStatementIndex === "number") {
59
+ appErr.statement_index = body.errorStatementIndex;
60
+ }
61
+ throw appErr;
62
+ }
63
+ /**
64
+ * 把 dataloom 业务错误码 + 原始 message 映射为语义化 AppError。
65
+ *
66
+ * 同时被两条错误路径共用:
67
+ * - ensureInnerSuccess(旧协议:BaseResp.status_code != "0")
68
+ * - api.ts 的 parseSqlErrorSentinel(新协议:success envelope + ERROR 哨兵)
69
+ *
70
+ * 优先级:
71
+ * 1. k_dl_1300002 + SQLSTATE 透传 → SQLSTATE_MAP
72
+ * 2. k_dl_1600000 + "Invalid DB Branch" → MULTI_ENV_NOT_INITIALIZED
73
+ * 3. BIZ_ERR_MAP 命中 → 语义 code(可能带 hint)
74
+ * 4. 兜底 DB_API_<code>
75
+ *
76
+ * stripPgPrefix 在这里做一次,调用方传 raw message 即可。
77
+ */
78
+ function mapDataloomBizError(code, rawMessage) {
79
+ const message = stripPgPrefix(rawMessage);
63
80
  if (code === "k_dl_1300002") {
64
81
  const sqlstate = extractSqlstate(message);
65
82
  if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
66
- throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message, { statement_index: stmtIdx });
83
+ return new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
67
84
  }
68
85
  }
69
- // k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
70
- // message 以 "Invalid DB Branch" 开头的两种触发场景:
71
- // 1) 单环境应用使用 --env(多环境未初始化)
72
- // 2) 多环境应用传入了不存在的 env 名(如 --env staging)
73
- // 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
74
86
  if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
75
- throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
76
- next_actions: [
77
- "Verify the --env value matches an existing dbBranch.",
78
- ],
79
- });
87
+ return new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", { next_actions: ["Verify the --env value matches an existing dbBranch."] });
80
88
  }
81
- // 业务 code 优先映射到语义 CLI code;未知 code 透传为 DB_API_<code> 样式
82
89
  const mapped = BIZ_ERR_MAP.get(code);
83
90
  if (mapped) {
84
- throw new error_1.AppError(mapped.code, mapped.message ?? message, {
91
+ return new error_1.AppError(mapped.code, mapped.message ?? message, {
85
92
  next_actions: mapped.hint ? [mapped.hint] : undefined,
86
- statement_index: stmtIdx,
87
93
  });
88
94
  }
89
- // 兜底:dataloom 未映射的 code 原样透传
90
- throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
95
+ return new error_1.AppError(`DB_API_${code}`, message);
91
96
  }
92
97
  /**
93
98
  * 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
@@ -111,16 +116,19 @@ function extractSqlstate(msg) {
111
116
  const BIZ_ERR_MAP = new Map(Object.entries({
112
117
  // 来源:真实环境探测(dataloom InnerExecuteSQL / InnerGetSchema)
113
118
  // k_dl_000001:DB 连接 / QPS 限流 等基础设施层错误
114
- "k_dl_000001": { code: "INTERNAL_DB_ERROR", hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试" },
119
+ k_dl_000001: {
120
+ code: "INTERNAL_DB_ERROR",
121
+ hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试",
122
+ },
115
123
  // k_dl_000002:SQL 解析 / 不支持的 SQL 类型(VACUUM、COPY、SET、SHOW 等)
116
- "k_dl_000002": { code: "SQL_SYNTAX_ERROR" },
124
+ k_dl_000002: { code: "SQL_SYNTAX_ERROR" },
117
125
  // k_dl_000003:查询命中系统表(pg_tables / pg_user 等)被拒
118
- "k_dl_000003": { code: "SQL_OPERATION_FORBIDDEN" },
126
+ k_dl_000003: { code: "SQL_OPERATION_FORBIDDEN" },
119
127
  // k_dl_1300002:PG 执行错误;实际 SQLSTATE 由 extractSqlstate 单独映射
120
128
  // 未匹配到 SQLSTATE 时走下面兜底 DB_API_<code>
121
129
  // k_dl_1300015:SELECT 结果超过 1000 行硬拦;多行 hint 由 output.ts 的
122
130
  // SERVER_ERROR_HINTS 按语义 code 兜底,这里只做 code 改名
123
- "k_dl_1300015": { code: "RESULT_SET_TOO_LARGE" },
131
+ k_dl_1300015: { code: "RESULT_SET_TOO_LARGE" },
124
132
  }));
125
133
  /** PG SQLSTATE → CLI code(当前 dataloom 不一定透传,预留未来使用) */
126
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", "INSERT", "UPDATE", "DELETE", "MERGE",
24
+ "SELECT",
25
+ "INSERT",
26
+ "UPDATE",
27
+ "DELETE",
28
+ "MERGE",
25
29
  // FROM / JOIN 系列
26
- "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "FULL", "CROSS", "USING",
30
+ "FROM",
31
+ "WHERE",
32
+ "JOIN",
33
+ "LEFT",
34
+ "RIGHT",
35
+ "INNER",
36
+ "OUTER",
37
+ "FULL",
38
+ "CROSS",
39
+ "USING",
27
40
  // 聚合 / 排序 / 分页
28
- "GROUP", "ORDER", "BY", "HAVING", "LIMIT", "OFFSET", "FETCH",
41
+ "GROUP",
42
+ "ORDER",
43
+ "BY",
44
+ "HAVING",
45
+ "LIMIT",
46
+ "OFFSET",
47
+ "FETCH",
29
48
  // 集合操作
30
- "UNION", "INTERSECT", "EXCEPT", "DISTINCT", "ALL",
49
+ "UNION",
50
+ "INTERSECT",
51
+ "EXCEPT",
52
+ "DISTINCT",
53
+ "ALL",
31
54
  // 别名 / 关联
32
- "AS", "ON",
55
+ "AS",
56
+ "ON",
33
57
  // 操作符词
34
- "AND", "OR", "NOT", "IN", "IS", "NULL", "TRUE", "FALSE",
35
- "BETWEEN", "LIKE", "ILIKE", "SIMILAR",
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", "ALTER", "DROP", "TRUNCATE", "RENAME",
38
- "TABLE", "INDEX", "VIEW", "DATABASE", "SCHEMA", "COLUMN", "CONSTRAINT", "SEQUENCE",
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", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CHECK", "DEFAULT",
85
+ "PRIMARY",
86
+ "KEY",
87
+ "FOREIGN",
88
+ "REFERENCES",
89
+ "UNIQUE",
90
+ "CHECK",
91
+ "DEFAULT",
41
92
  // 写入
42
- "VALUES", "SET", "RETURNING", "INTO",
93
+ "VALUES",
94
+ "SET",
95
+ "RETURNING",
96
+ "INTO",
43
97
  // 事务
44
- "BEGIN", "COMMIT", "ROLLBACK", "SAVEPOINT", "TRANSACTION",
98
+ "BEGIN",
99
+ "COMMIT",
100
+ "ROLLBACK",
101
+ "SAVEPOINT",
102
+ "TRANSACTION",
45
103
  // 控制流 / CTE
46
- "IF", "EXISTS", "REPLACE", "WITH", "RECURSIVE",
47
- "CASE", "WHEN", "THEN", "ELSE", "END",
104
+ "IF",
105
+ "EXISTS",
106
+ "REPLACE",
107
+ "WITH",
108
+ "RECURSIVE",
109
+ "CASE",
110
+ "WHEN",
111
+ "THEN",
112
+ "ELSE",
113
+ "END",
48
114
  // 类型转换 / 时间提取
49
- "CAST", "EXTRACT",
115
+ "CAST",
116
+ "EXTRACT",
50
117
  // 排序方向
51
- "ASC", "DESC", "NULLS", "FIRST", "LAST",
118
+ "ASC",
119
+ "DESC",
120
+ "NULLS",
121
+ "FIRST",
122
+ "LAST",
52
123
  ];
@@ -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, { errorContext: "upload callback" });
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) {
@@ -378,19 +378,31 @@ async function uploadFile(opts) {
378
378
  throw new error_1.AppError("FILE_UPLOAD_FAILED", "Upload PUT returned no ETag");
379
379
  }
380
380
  // callback 返回服务端实际生成的对象元数据(filePath / file_name / download_url)。
381
- // 这是 CLI 拿到真实存储路径的唯一来源——preUpload 不暴露 path,避免客户端再做拼接。
382
- // callback 网络失败 / metadata 解析失败时仍按"上传成功"处理:文件已经在对象存储里,
383
- // 缺失的字段(如 download_url)只是展示降级;用本地已知信息兜底。
384
- let metadata = {};
381
+ // path 末段是平台生成的 16 位 GID,CLI 无法靠本地信息推断;callback 失败 /
382
+ // metadata.filePath 缺失时直接抛 FILE_UPLOAD_CALLBACK_INCOMPLETE:对象已落
383
+ // TOS(PUT 已成功 + ETag 拿到),但 CLI 拿不到真实 path。把 fileName 写到
384
+ // hint 里引导用户走 `file ls --name` 查实际 path,避免返一个假 path 让后续
385
+ // stat/download/rm 全部 FILE_NOT_FOUND。
386
+ let metadata;
385
387
  try {
386
388
  metadata = await uploadCallback(opts.appId, { uploadID: pre.uploadID, eTag: etag });
387
389
  }
388
390
  catch (err) {
389
- (0, logger_1.debug)(`upload callback failed: ${err instanceof Error ? err.message : String(err)}`);
391
+ const reason = err instanceof Error ? err.message : String(err);
392
+ throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", `Upload callback failed: ${reason}; file may already exist in storage`, {
393
+ next_actions: [
394
+ `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
395
+ ],
396
+ });
397
+ }
398
+ if (!metadata.filePath) {
399
+ throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", "Upload callback returned no filePath; file may already exist in storage", {
400
+ next_actions: [
401
+ `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
402
+ ],
403
+ });
390
404
  }
391
- const path = metadata.filePath
392
- ? (metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath)
393
- : (opts.remotePath ?? "/" + opts.fileName);
405
+ const path = metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath;
394
406
  const result = {
395
407
  // 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
396
408
  // 与后续 file ls / file stat 返回的展示名保持一致;缺失时降级用本地 fileName。
@@ -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
  }
@@ -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
  /**
@@ -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}`, { next_actions: ["检查包名拼写,或确认该插件已在插件市场发布"] });
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}`, { next_actions: ["插件 key 必须形如 @scope/name"] });
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 ?? new error_1.AppError("INTERNAL_RETRY_EXHAUSTED", `${description} failed after ${String(maxRetries)} retries`, { retryable: true, next_actions: ["检查网络后重试,--verbose 可查看重试日志"] });
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) => { await (0, index_1.handlePluginInstall)({ 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) => { await (0, index_1.handlePluginUpdate)({ 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) => { (0, index_1.handlePluginRemove)({ 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 () => { await (0, index_1.handlePluginInit)(); });
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) => { await (0, index_1.handlePluginList)(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(() => { (0, index_1.handlePluginListPlugins)(); });
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`, { next_actions: ["Check the file path."] });
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`, { next_actions: ["Use -f to specify a different path, or --force to overwrite."] });
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)`, { next_actions: [`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`] });
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({ appId, format: "schema", includeStats: true, dbBranch: opts.env });
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 ? "—" : (tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes)),
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.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
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": return "inserted";
353
- case "UPDATE": return "updated";
354
- case "DELETE": return "deleted";
355
- case "MERGE": return "merged";
356
- case "DML": return "affected"; // 未识别子类的兜底
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
  /** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
@@ -491,33 +499,31 @@ async function loadTableMap(ctx) {
491
499
  }
492
500
  function enrichRelationNotExist(err, relation, tableMap) {
493
501
  const guess = (0, fuzzy_match_1.suggest)(relation, Object.keys(tableMap));
494
- if (guess) {
495
- err.next_actions.push(`Did you mean '${guess}'? Run \`miaoda db schema list\` to see all tables.`);
496
- }
502
+ err.next_actions.push(guess
503
+ ? `Did you mean '${guess}'? Run \`miaoda db schema list\` to see all tables.`
504
+ : "Run `miaoda db schema list` to see all tables.");
497
505
  }
498
506
  function enrichColumnNotExist(err, colMatch, tableMap) {
499
507
  const colName = colMatch[1];
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);
509
- if (guess) {
510
- err.next_actions.push(`Did you mean column '${guess}' in table '${relation}'?`);
511
- }
512
- }
513
- else {
514
- // 没指定表(CTE / 别名 / 子查询场景)→ 在所有表的列合集里找
515
- const allCols = Array.from(new Set(Object.values(tableMap).flat()));
516
- const guess = (0, fuzzy_match_1.suggest)(colName, allCols);
517
- if (guess) {
518
- err.next_actions.push(`Did you mean '${guess}'? Run \`miaoda db schema get <table>\` to see columns.`);
519
- }
515
+ err.next_actions.push(guess
516
+ ? `Did you mean column '${guess}' in table '${relation}'?`
517
+ : `Run \`miaoda db schema get ${relation}\` to see all columns.`);
518
+ return;
520
519
  }
520
+ // 没指定表(CTE / 别名 / 子查询 / 单表 SELECT 但 PG 报错没带 relation 等场景)
521
+ // → 跨表所有列合集 fuzzy;没命中也给一句通用 hint,让用户知道下一步去哪查。
522
+ const allCols = Array.from(new Set(Object.values(tableMap).flat()));
523
+ const guess = (0, fuzzy_match_1.suggest)(colName, allCols);
524
+ err.next_actions.push(guess
525
+ ? `Did you mean '${guess}'? Run \`miaoda db schema get <table>\` to see columns.`
526
+ : "Run `miaoda db schema get <table>` to see all columns.");
521
527
  }
522
528
  /**
523
529
  * 扫整段 SQL 找一个看似"拼错的关键字"token:纯字母 + 长度 ≥ 3 + 不是已知 keyword
@@ -570,14 +576,18 @@ function extractTableFieldMap(s) {
570
576
  function enrichMultiStatementError(err, sql) {
571
577
  if (!(err instanceof error_1.AppError))
572
578
  return;
573
- const partial = err.partial_results;
574
- if (!Array.isArray(partial) || partial.length === 0)
579
+ // SQL 文本判断是否多语句,而不是 partial.length——第一条就失败时
580
+ // partial_results=[],但仍需输出统一的多语句 envelope 字段保持格式一致。
581
+ const total = countStatements(sql);
582
+ if (total <= 1)
575
583
  return;
576
- // SQLExecuteResult[] 转成 PRD 兼容的 completed 数组
584
+ const partial = Array.isArray(err.partial_results)
585
+ ? err.partial_results
586
+ : [];
577
587
  const completed = partial.map((r) => toMultiElement((0, index_1.parseSqlResult)(r)));
578
588
  err.completed = completed;
579
589
  err.rolled_back = inferRolledBack(completed);
580
- err.total_statements = countStatements(sql);
590
+ err.total_statements = total;
581
591
  // pretty 模式(非 JSON)打 per-statement breakdown 到 stderr
582
592
  if (!(0, output_1.isJsonMode)()) {
583
593
  writeMultiStatementBreakdown(err, completed);
@@ -609,9 +619,7 @@ function inferRolledBack(completed) {
609
619
  * dollar-quoted($$..$$)等高级形态——估错 ±1 不影响 hint 可读性。
610
620
  */
611
621
  function countStatements(sql) {
612
- const stripped = sql
613
- .replace(/'(?:''|[^'])*'/g, "''")
614
- .replace(/"(?:""|[^"])*"/g, '""');
622
+ const stripped = sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""');
615
623
  return stripped.split(/;+/).filter((s) => s.trim().length > 0).length;
616
624
  }
617
625
  /**
@@ -48,13 +48,20 @@ const shared_1 = require("../../../cli/commands/shared");
48
48
  const colors_1 = require("../../../utils/colors");
49
49
  const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
50
50
  /**
51
- * 判断 src 是本地文件还是远程引用:
52
- * - src 本地 fs 可访问 upload(dst remote)
53
- * - 其他 → download(src 是 /path 或 file_name,由 resolveRemotePath 解析)
54
- *
55
- * `cp` 语义要求 src 必须存在;本地不存在就认为用户指向远程。不需要给 dst 猜方向。
51
+ * 判断 src 是本地路径还是远程引用——按 PRD `miaoda file cp` 规则的优先级:
52
+ * 1. `./` / `../` / `~/` 开头 + 裸文件名(不含 `/`)→ 本地路径(即使不存在)
53
+ * handleUpload 把"不存在"准确抛成 FILE_SRC_NOT_FOUND,
54
+ * 避免回退到远程 download 输出 `FILE_NOT_FOUND` + "Run miaoda file ls"
55
+ * 这种与用户意图(上传)背离的引导。
56
+ * 2. `/` 开头:fs.existsSync 兜底——存在即本地(绝对路径上传),
57
+ * 不存在即远程 path(download,由 resolveRemotePath 处理)。
56
58
  */
57
59
  function isLocalSrc(src) {
60
+ if (src.startsWith("./") || src.startsWith("../") || src.startsWith("~/"))
61
+ return true;
62
+ if (!src.includes("/"))
63
+ return true;
64
+ // `/` 开头:可能是本地绝对路径,也可能是远程 path;交给 fs 探测。
58
65
  const expanded = expandHome(src);
59
66
  try {
60
67
  return node_fs_1.default.existsSync(expanded);
@@ -105,24 +112,38 @@ async function resolveRemotePath(appId, input) {
105
112
  async function handleUpload(appId, localRaw, remoteRaw, rename) {
106
113
  const localPath = expandHome(localRaw);
107
114
  if (!node_fs_1.default.existsSync(localPath) || !node_fs_1.default.statSync(localPath).isFile()) {
108
- throw new error_1.AppError("FILE_SRC_NOT_FOUND", `Local file '${localRaw}' does not exist`);
115
+ throw new error_1.AppError("FILE_SRC_NOT_FOUND", `Local file '${localRaw}' does not exist`, {
116
+ // 引导本地路径自检;远程下载请用 `/path` 形态,避免裸名/相对路径误入上传分支
117
+ next_actions: ["Check the local file path; use `/path` prefix for remote download."],
118
+ });
109
119
  }
110
120
  const stat = node_fs_1.default.statSync(localPath);
111
121
  if (stat.size > MAX_UPLOAD_BYTES) {
112
122
  throw new error_1.AppError("FILE_SIZE_EXCEEDED", `File size ${(0, render_1.formatSize)(stat.size)} exceeds the 100 MB upload limit`, {
113
- next_actions: [
114
- "Split the file, or use the web console for large uploads.",
115
- ],
123
+ next_actions: ["Split the file, or use the web console for large uploads."],
116
124
  });
117
125
  }
118
- const fileName = rename ?? node_path_1.default.basename(localPath);
119
- // remoteRaw 直接透传给服务端,由服务端按"是否以 / 结尾"区分两种语义:
120
- // - / 结尾 → 目录前缀模式,服务端在前缀下生成 "<16位ID>.<扩展名>"
121
- // 例:cp ./logo.png /images//images/1858537546760216.png
122
- // - 不以 / 结尾 完整对象 key 模式(旧语义,原样落库)
123
- // 例:cp ./photo.jpg /uploads/photo.jpg/uploads/photo.jpg
124
- // CLI 不做任何自动改写,避免猜测用户意图导致的 path 形态不可预测。
125
- const remotePath = remoteRaw;
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
+ }
126
147
  const contentType = detectMime(localPath);
127
148
  const result = await api.file.uploadFile({
128
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 ?? (p.split("/").pop() ?? p),
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", { next_actions: ["在应用项目根目录运行"] });
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"], { cwd: getProjectRoot(), stdio: "inherit" });
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}`, { next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"] });
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}`, { next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"] });
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}`, { next_actions: ["运行 miaoda plugin list 查看所有可用 capability 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`, { next_actions: [`检查 server/capabilities/${id}.json 的 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}`, { next_actions: [`运行 miaoda plugin install ${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) => { (0, logger_1.log)("plugin", 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
- api.plugin.reportCreateInstanceEvent(name, installedVersion).catch(() => { });
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`, { next_actions: ["运行 miaoda plugin list-packages 查看已安装插件"] });
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", { next_actions: ["当前目录必须是含 server/capabilities/ 的应用项目"] });
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.visibleCommands(cmd).map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
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.visibleOptions(cmd)
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) {
@@ -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
- "RESULT_SET_TOO_LARGE": [
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 += typeof n === "number" && n > 0
94
- ? ` (at statement ${String(k)} of ${String(n)})`
95
- : ` (at statement ${String(k)})`;
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: " 之后的列。
@@ -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 <= 0x115F) || // Hangul Jamo
85
- cp === 0x2329 || cp === 0x232A ||
86
- (cp >= 0x2E80 && cp <= 0x303E) || // CJK Radicals / Punctuation
87
- (cp >= 0x3041 && cp <= 0x33FF) || // Hiragana / Katakana / CJK Symbols
88
- (cp >= 0x3400 && cp <= 0x4DBF) || // CJK Ext A
89
- (cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified
90
- (cp >= 0xA000 && cp <= 0xA4CF) || // Yi
91
- (cp >= 0xAC00 && cp <= 0xD7A3) || // Hangul Syllables
92
- (cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compat Ideographs
93
- (cp >= 0xFE30 && cp <= 0xFE4F) || // CJK Compat Forms
94
- (cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms
95
- (cp >= 0xFFE0 && cp <= 0xFFE6) ||
96
- (cp >= 0x20000 && cp <= 0x2FFFD) || // CJK Ext B-F
97
- (cp >= 0x30000 && cp <= 0x3FFFD) // CJK Ext G-H
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.map((h, i) => colors_1.c.header(padVisibleEnd(h, colWidths[i]))).join(" ").trimEnd());
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.map((cell, i) => padVisibleEnd(cell || "", colWidths[i])).join(" ").trimEnd());
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": return n;
171
- case "m": return n * 60;
172
- case "h": return n * 3600;
173
- case "d": return n * 86400;
174
- default: return n;
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": return Math.round(n);
187
- case "KB": return Math.round(n * 1024);
188
- case "MB": return Math.round(n * 1024 * 1024);
189
- case "GB": return Math.round(n * 1024 * 1024 * 1024);
190
- default: return Math.round(n);
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.e70b415",
3
+ "version": "0.1.1-alpha.eccb0b7",
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",