@lark-apaas/miaoda-cli 0.1.0-alpha.8825a76 → 0.1.0-alpha.8dcc262

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.
@@ -5,8 +5,6 @@ exports.getSchema = getSchema;
5
5
  exports.importData = importData;
6
6
  exports.exportData = exportData;
7
7
  const http_1 = require("../../utils/http");
8
- // TODO(REMOVE-BEFORE-RELEASE): debug-only HTTP trace(详见 _debug_trace.ts)
9
- const _debug_trace_1 = require("./_debug_trace");
10
8
  const error_1 = require("../../utils/error");
11
9
  const client_1 = require("./client");
12
10
  // CLI 不再为 dbBranch 设默认值:
@@ -26,9 +24,7 @@ async function execSql(opts) {
26
24
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
27
25
  dbBranch: opts.dbBranch,
28
26
  });
29
- (0, _debug_trace_1.traceRequest)("POST", url);
30
27
  const response = await client.post(url, { sql: opts.sql });
31
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers);
32
28
  if (!response.ok) {
33
29
  // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
34
30
  let body = null;
@@ -61,9 +57,7 @@ async function getSchema(opts) {
61
57
  includeStats: opts.includeStats ? "true" : undefined,
62
58
  dbBranch: opts.dbBranch,
63
59
  });
64
- (0, _debug_trace_1.traceRequest)("GET", url);
65
60
  const response = await client.get(url);
66
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers);
67
61
  if (!response.ok) {
68
62
  let body = null;
69
63
  try {
@@ -95,14 +89,12 @@ async function importData(opts) {
95
89
  });
96
90
  const contentType = opts.format === "csv" ? "text/csv" : "application/json";
97
91
  const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
98
- (0, _debug_trace_1.traceRequest)("POST", url);
99
92
  const response = await client.request({
100
93
  method: "POST",
101
94
  url,
102
95
  headers: { "Content-Type": contentType },
103
96
  body: ab,
104
97
  });
105
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers);
106
98
  if (!response.ok) {
107
99
  let body = null;
108
100
  try {
@@ -139,9 +131,7 @@ async function exportData(opts) {
139
131
  dbBranch: opts.dbBranch,
140
132
  });
141
133
  const reqBody = { limit: opts.limit ?? 5000 };
142
- (0, _debug_trace_1.traceRequest)("POST", url);
143
134
  const response = await client.post(url, reqBody);
144
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers);
145
135
  if (!response.ok) {
146
136
  // 错误路径:body 是 JSON envelope
147
137
  let body = null;
@@ -19,12 +19,16 @@ function ensureInnerSuccess(body) {
19
19
  const code = body.status_code ?? body.ErrorCode ?? "0";
20
20
  if (code === "0" || code === "")
21
21
  return;
22
- const message = body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`;
22
+ const message = stripPgPrefix(body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
23
+ // PRD 多语句失败:后端在 envelope 顶层透出 errorStatementIndex(从 0 起计),
24
+ // 单语句 / 单元执行不会带这个字段。下面的 AppError 都把它带上,让最终
25
+ // CLI JSON envelope 写到 error.statement_index。
26
+ const stmtIdx = typeof body.errorStatementIndex === "number" ? body.errorStatementIndex : undefined;
23
27
  // k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
24
28
  if (code === "k_dl_1300002") {
25
29
  const sqlstate = extractSqlstate(message);
26
30
  if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
27
- throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
31
+ throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message, { statement_index: stmtIdx });
28
32
  }
29
33
  }
30
34
  // k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
@@ -35,7 +39,7 @@ function ensureInnerSuccess(body) {
35
39
  if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
36
40
  throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
37
41
  next_actions: [
38
- "Verify the --env value matches an existing dbBranch, or run `miaoda db migration init` to set up multi-env for this app.",
42
+ "Verify the --env value matches an existing dbBranch.",
39
43
  ],
40
44
  });
41
45
  }
@@ -44,10 +48,19 @@ function ensureInnerSuccess(body) {
44
48
  if (mapped) {
45
49
  throw new error_1.AppError(mapped.code, mapped.message ?? message, {
46
50
  next_actions: mapped.hint ? [mapped.hint] : undefined,
51
+ statement_index: stmtIdx,
47
52
  });
48
53
  }
49
54
  // 兜底:dataloom 未映射的 code 原样透传
50
- throw new error_1.AppError(`DB_API_${code}`, message);
55
+ throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
56
+ }
57
+ /**
58
+ * 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
59
+ * 不去掉就会变成 `Error: ERROR: relation ...` 双重前缀,PRD 不要这种冗余。
60
+ * 大小写敏感,只匹配 PG 标准格式。
61
+ */
62
+ function stripPgPrefix(msg) {
63
+ return msg.replace(/^ERROR:\s*/, "");
51
64
  }
52
65
  /** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
53
66
  function extractSqlstate(msg) {
@@ -27,16 +27,23 @@ function parseSqlResult(r) {
27
27
  recordCount: r.recordCount ?? rows.length,
28
28
  };
29
29
  }
30
- if (r.sqlType === "INSERT" || r.sqlType === "UPDATE" || r.sqlType === "DELETE") {
30
+ if (r.sqlType === "INSERT" ||
31
+ r.sqlType === "UPDATE" ||
32
+ r.sqlType === "DELETE" ||
33
+ r.sqlType === "MERGE" ||
34
+ r.sqlType === "DML") {
31
35
  const affected = r.affectedRows ?? extractRowCount(r.data);
32
36
  return {
33
37
  kind: "dml",
38
+ // 上面已 narrow,这里 cast 是为了 SqlType 联合里的 (string & {}) 让 TS 无法
39
+ // 自动收窄到字面量集合,不影响运行时安全
34
40
  sqlType: r.sqlType,
35
41
  affectedRows: affected,
36
42
  };
37
43
  }
38
- // DDL or unknown
39
- return { kind: "ddl" };
44
+ // DDL or unknown — sqlType 透传后端给的细粒度(CREATE_TABLE / DROP_TABLE / ...
45
+ // / 笼统 "DDL"),CLI JSON 输出直接当 command 用
46
+ return { kind: "ddl", sqlType: r.sqlType };
40
47
  }
41
48
  /** DML 的 data 通常是 `[{"rowCount": N}]`;兜底从这里读影响行数。 */
42
49
  function extractRowCount(data) {
@@ -82,7 +89,6 @@ function toSummary(t, stats) {
82
89
  columns: (t.fields ?? []).length,
83
90
  estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
84
91
  size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
85
- updated_at: t.updatedAt,
86
92
  };
87
93
  }
88
94
  /**
@@ -118,8 +124,6 @@ function toDetail(t, stats) {
118
124
  indexes: rawIndexes.map(toIndex),
119
125
  estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
120
126
  size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
121
- created_at: t.createdAt,
122
- updated_at: t.updatedAt,
123
127
  };
124
128
  }
125
129
  function toColumn(f) {
@@ -214,7 +214,7 @@ async function resolveByName(appId, input) {
214
214
  error: {
215
215
  code: "FILE_NOT_FOUND",
216
216
  message: `File '${input}' does not exist`,
217
- hint: "Run `miaoda file ls` to verify file_name.",
217
+ hint: "Run `miaoda file ls` to see available files.",
218
218
  },
219
219
  };
220
220
  }
@@ -225,7 +225,7 @@ async function resolveByName(appId, input) {
225
225
  error: {
226
226
  code: "AMBIGUOUS_FILE_NAME",
227
227
  message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
228
- hint: `Use absolute /path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
228
+ hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
229
229
  },
230
230
  };
231
231
  }
@@ -8,8 +8,6 @@ exports.doPost = doPost;
8
8
  exports.doRequest = doRequest;
9
9
  const http_1 = require("../../utils/http");
10
10
  const error_1 = require("../../utils/error");
11
- // TODO(REMOVE-BEFORE-RELEASE): debug-only HTTP trace(详见 _debug_trace.ts)
12
- const _debug_trace_1 = require("./_debug_trace");
13
11
  const http_client_1 = require("@lark-apaas/http-client");
14
12
  /** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
15
13
  const bucketCache = new Map();
@@ -133,15 +131,10 @@ async function mapHttpError(err, opts) {
133
131
  */
134
132
  async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
135
133
  try {
136
- (0, _debug_trace_1.traceRequest)("GET", url);
137
134
  const response = await client.get(url);
138
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers);
139
135
  return (await response.json());
140
136
  }
141
137
  catch (err) {
142
- if (err instanceof http_client_1.HttpError && err.response) {
143
- (0, _debug_trace_1.traceResponse)("GET", url, err.response.status, err.response.headers);
144
- }
145
138
  await mapHttpError(err, opts);
146
139
  throw err; // 不可达,mapHttpError 必定 throw
147
140
  }
@@ -149,15 +142,10 @@ async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
149
142
  /** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
150
143
  async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
151
144
  try {
152
- (0, _debug_trace_1.traceRequest)("POST", url);
153
145
  const response = await client.post(url, body);
154
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers);
155
146
  return (await response.json());
156
147
  }
157
148
  catch (err) {
158
- if (err instanceof http_client_1.HttpError && err.response) {
159
- (0, _debug_trace_1.traceResponse)("POST", url, err.response.status, err.response.headers);
160
- }
161
149
  await mapHttpError(err, opts);
162
150
  throw err;
163
151
  }
@@ -165,15 +153,10 @@ async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()
165
153
  /** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
166
154
  async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
167
155
  try {
168
- (0, _debug_trace_1.traceRequest)(cfg.method, cfg.url);
169
156
  const response = await client.request(cfg);
170
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, response.status, response.headers);
171
157
  return (await response.json());
172
158
  }
173
159
  catch (err) {
174
- if (err instanceof http_client_1.HttpError && err.response) {
175
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, err.response.status, err.response.headers);
176
- }
177
160
  await mapHttpError(err, opts);
178
161
  throw err;
179
162
  }
@@ -28,7 +28,7 @@ function registerDbCommands(program) {
28
28
  });
29
29
  schemaCmd
30
30
  .command("list")
31
- .description("列出应用所有表(rows / size / columns / updated_at)")
31
+ .description("列出应用所有表(estimated_row_count / size / columns)")
32
32
  .addOption((0, shared_1.appIdOption)())
33
33
  .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
34
34
  .action(async (opts) => {
@@ -3,6 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerFileCommands = registerFileCommands;
4
4
  const index_1 = require("../../../cli/handlers/file/index");
5
5
  const shared_1 = require("../../../cli/commands/shared");
6
+ const error_1 = require("../../../utils/error");
7
+ /**
8
+ * commander option 校验器:把 --limit <n> 解析成正整数(≥1)。
9
+ * 默认值(如 "50")会先经过这里被规范化成 number。
10
+ * 非整数 / 负数 / 0 抛 AppError("ARGS_INVALID"),由 main.ts 的全局 catch
11
+ * 走 emitError,同时 process.exitCode 由 commander 自然为 1。
12
+ */
13
+ function parsePositiveInt(raw) {
14
+ const n = Number(raw);
15
+ if (!Number.isInteger(n) || n < 1) {
16
+ throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
17
+ }
18
+ return n;
19
+ }
6
20
  function registerFileCommands(program) {
7
21
  const fileCmd = program
8
22
  .command("file")
@@ -21,7 +35,7 @@ function registerFileCommands(program) {
21
35
  .option("--size-gt <size>", "大小大于,支持 B/KB/MB/GB")
22
36
  .option("--size-lt <size>", "大小小于")
23
37
  .option("--uploaded-since <time>", "上传时间晚于(ISO 8601)")
24
- .option("--limit <n>", "返回条数上限", "50")
38
+ .option("--limit <n>", "返回条数上限(正整数,默认 50)", parsePositiveInt, 50)
25
39
  .option("--cursor <token>", "分页游标")
26
40
  .option("--all", "自动翻页聚合全部结果")
27
41
  .action(async (query, opts) => {
@@ -7,7 +7,9 @@ exports.withHelp = withHelp;
7
7
  exports.failArgs = failArgs;
8
8
  const commander_1 = require("commander");
9
9
  const error_1 = require("../../utils/error");
10
- /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption()) */
10
+ /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption())
11
+ * Commander 的 .env() 只接受单个变量名,第二个兜底 `app_id` 在 resolveAppId 里手动检查。
12
+ */
11
13
  function appIdOption() {
12
14
  return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
13
15
  }
@@ -17,9 +19,12 @@ function appIdOption() {
17
19
  function softRequiredOption(name, desc) {
18
20
  return new commander_1.Option(name, desc);
19
21
  }
20
- /** 解析 appId,CLI flag > env > 抛错 */
22
+ /**
23
+ * 解析 appId,优先级:CLI flag > MIAODA_APP_ID > app_id > 抛错。
24
+ * app_id 是部分外部沙箱环境注入应用 ID 的别名,作为兜底兼容(小写下划线形态)。
25
+ */
21
26
  function resolveAppId(opts) {
22
- const id = opts.appId ?? process.env.MIAODA_APP_ID;
27
+ const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
23
28
  if (!id) {
24
29
  throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
25
30
  next_actions: [
@@ -55,14 +55,14 @@ async function handleDbSchemaList(opts) {
55
55
  return;
56
56
  }
57
57
  const tty = (0, render_1.isStdoutTty)();
58
- // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)
58
+ // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)。
59
+ // updated_at 暂时不展示——PG pg_catalog 不存真实表时间,详见 renderDetail 注释。
59
60
  const headers = [
60
61
  "name",
61
62
  "description",
62
63
  "estimated_row_count",
63
64
  tty ? "size" : "size_bytes",
64
65
  "columns",
65
- "updated_at",
66
66
  ];
67
67
  const rows = tables.map((t) => [
68
68
  t.name,
@@ -70,7 +70,6 @@ async function handleDbSchemaList(opts) {
70
70
  t.estimated_row_count === null ? "—" : String(t.estimated_row_count),
71
71
  t.size_bytes === null ? "—" : (tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes)),
72
72
  String(t.columns),
73
- (0, render_1.formatTime)(t.updated_at, tty),
74
73
  ]);
75
74
  (0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
76
75
  }
@@ -123,7 +122,10 @@ async function handleDbSchemaGet(table, opts) {
123
122
  function renderDetail(d, tty) {
124
123
  const systemFields = d.columns.filter((c) => c.name.startsWith("_"));
125
124
  const userFields = d.columns.filter((c) => !c.name.startsWith("_"));
126
- // PRD 的 header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size / Created / Updated
125
+ // header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size
126
+ // 不展示 Created / Updated:PG pg_catalog 不存表创建时间,dataloom 用 OID
127
+ // 构造的伪时间戳(baseTime=2020-01-01 + OID 秒偏移),仅保排序意义、绝对值
128
+ // 误导性强,先去掉。后续如果接 ddl_change_log 取真实时间再加回。
127
129
  const header = [
128
130
  ["Name", d.name],
129
131
  ["Description", d.description ?? "—"],
@@ -135,8 +137,6 @@ function renderDetail(d, tty) {
135
137
  ],
136
138
  ["Estimated Rows", d.estimated_row_count === null ? "—" : String(d.estimated_row_count)],
137
139
  ["Size", d.size_bytes === null ? "—" : (0, render_1.formatSize)(d.size_bytes)],
138
- ["Created", (0, render_1.formatTime)(d.created_at, tty)],
139
- ["Updated", (0, render_1.formatTime)(d.updated_at, tty)],
140
140
  ];
141
141
  const colHeaders = ["column", "type", "nullable", "default", "comment"];
142
142
  const colRows = userFields.map((c) => [
@@ -37,6 +37,7 @@ exports.handleDbSql = handleDbSql;
37
37
  const api = __importStar(require("../../../api/index"));
38
38
  const error_1 = require("../../../utils/error");
39
39
  const output_1 = require("../../../utils/output");
40
+ const config_1 = require("../../../utils/config");
40
41
  const shared_1 = require("../../../cli/commands/shared");
41
42
  const render_1 = require("../../../utils/render");
42
43
  const index_1 = require("../../../api/db/index");
@@ -47,7 +48,8 @@ const index_1 = require("../../../api/db/index");
47
48
  * - --env 透传给后端 admin-inner(dbBranch 参数),不传时由后端按 workspace
48
49
  * 多环境状态兜底(多环境 → dev / 单环境 → main);后端检测到环境不存在
49
50
  * 会返 k_dl_1600000 + "Invalid DB Branch:...",CLI 侧映射为 MULTI_ENV_NOT_INITIALIZED
50
- * - 多条语句时只展示最后一条的结果(与 PRD 对齐);所有为 DDL 时展示批量摘要
51
+ * - 多语句行为对齐 PRD:每条 statement 一个独立结果元素,pretty 逐条 +
52
+ * 末尾汇总,--json 输出 data 数组。
51
53
  */
52
54
  async function handleDbSql(query, opts) {
53
55
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -65,22 +67,16 @@ async function handleDbSql(query, opts) {
65
67
  (0, output_1.emit)("✓ No results");
66
68
  return;
67
69
  }
68
- // 全部 DDL 的批量场景:展示统计摘要(对齐 PRD `✓ 3 statements executed`)
69
- const allDdl = results.every((r) => r.sqlType === "DDL");
70
- if (allDdl && results.length > 1) {
71
- if ((0, output_1.isJsonMode)()) {
72
- (0, output_1.emit)({ data: { statements: results.length } });
73
- return;
74
- }
75
- const tty = (0, render_1.isStdoutTty)();
76
- (0, output_1.emit)(tty
77
- ? `✓ ${String(results.length)} statements executed`
78
- : `OK ${String(results.length)} statements executed`);
70
+ if (results.length === 1) {
71
+ renderSingle(results[0]);
72
+ return;
73
+ }
74
+ // 多语句:每条 statement 独立结果
75
+ if ((0, output_1.isJsonMode)()) {
76
+ (0, output_1.emit)({ data: results.map((r) => toMultiElement((0, index_1.parseSqlResult)(r))) });
79
77
  return;
80
78
  }
81
- // 其他场景:展示最后一条
82
- const last = results[results.length - 1];
83
- renderSingle(last);
79
+ renderMultiPretty(results);
84
80
  }
85
81
  /** 读取 stdin 并返回完整 SQL 文本(stdin 不是 TTY 即认为被 pipe)。 */
86
82
  async function readSql(inline) {
@@ -130,23 +126,102 @@ function renderSingle(raw) {
130
126
  }
131
127
  function toJson(parsed) {
132
128
  if (parsed.kind === "select") {
133
- return { data: parsed.rows };
129
+ // PRD 单 SELECT:data 直接是行数组(按 --json 字段投影裁剪)
130
+ return { data: projectRows(parsed.rows) };
134
131
  }
135
132
  if (parsed.kind === "dml") {
133
+ // PRD 单 DML:data = {command, rows_affected}
136
134
  return {
137
135
  data: {
138
- sql_type: parsed.sqlType.toLowerCase(),
139
- affected_rows: parsed.affectedRows,
136
+ command: parsed.sqlType,
137
+ rows_affected: parsed.affectedRows,
140
138
  },
141
139
  };
142
140
  }
143
- return { data: { sql_type: "ddl" } };
141
+ // PRD 单 DDL:data = {command, target?}。command 直接用后端给的细粒度
142
+ // (CREATE_TABLE / DROP_TABLE / ...),target 待后端给对象名后再加。
143
+ return { data: { command: parsed.sqlType } };
144
+ }
145
+ /**
146
+ * 多语句 --json 元素:与单 DDL/DML 形状一致,但 SELECT 包成
147
+ * {command:"SELECT", rows:[...]}(PRD 约定,避免数组里嵌套数组造成歧义)。
148
+ */
149
+ function toMultiElement(parsed) {
150
+ if (parsed.kind === "select") {
151
+ return { command: "SELECT", rows: projectRows(parsed.rows) };
152
+ }
153
+ if (parsed.kind === "dml") {
154
+ return { command: parsed.sqlType, rows_affected: parsed.affectedRows };
155
+ }
156
+ // DDL:用后端给的细粒度 command
157
+ return { command: parsed.sqlType };
158
+ }
159
+ /**
160
+ * PRD:`--json id,name` 字段投影。--json 不带值(boolean true)等价于不裁剪。
161
+ * 字段不存在时按 undefined 处理(JSON.stringify 会忽略 undefined value 的 key),
162
+ * 这样 Agent 拿到的 row 永远只含请求过的列。
163
+ */
164
+ function projectRows(rows) {
165
+ const fields = parseJsonFields();
166
+ if (!fields)
167
+ return rows;
168
+ return rows.map((r) => {
169
+ const out = {};
170
+ for (const f of fields) {
171
+ out[f] = r[f];
172
+ }
173
+ return out;
174
+ });
175
+ }
176
+ /** 读取 --json [fields]:返回字段列表;boolean true 或 undefined 返回 null(不裁剪)。 */
177
+ function parseJsonFields() {
178
+ const v = (0, config_1.getConfig)().json;
179
+ if (typeof v !== "string" || v === "")
180
+ return null;
181
+ return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
182
+ }
183
+ /** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
184
+ function renderMultiPretty(results) {
185
+ const tty = (0, render_1.isStdoutTty)();
186
+ const lines = [];
187
+ for (let i = 0; i < results.length; i++) {
188
+ const parsed = (0, index_1.parseSqlResult)(results[i]);
189
+ const idx = i + 1;
190
+ if (parsed.kind === "select") {
191
+ const n = parsed.rows.length;
192
+ lines.push(`Statement ${String(idx)}: SELECT (${String(n)} row${n === 1 ? "" : "s"})`);
193
+ if (n > 0) {
194
+ const cols = collectColumns(parsed.rows);
195
+ const tbl = parsed.rows.map((r) => cols.map((c) => formatCell(r[c], tty)));
196
+ lines.push(tty ? (0, render_1.renderAlignedTable)(cols, tbl) : (0, render_1.renderTsv)(cols, tbl));
197
+ }
198
+ // 块间空行(最后一条不留)
199
+ if (i < results.length - 1)
200
+ lines.push("");
201
+ continue;
202
+ }
203
+ if (parsed.kind === "dml") {
204
+ const verb = dmlVerb(parsed.sqlType);
205
+ const n = parsed.affectedRows;
206
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}${String(n)} row${n === 1 ? "" : "s"} ${verb}`);
207
+ continue;
208
+ }
209
+ // DDL
210
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}DDL executed`);
211
+ }
212
+ // 汇总行:所有 statement 都跑完了
213
+ lines.push(tty
214
+ ? `✓ ${String(results.length)} statements executed`
215
+ : `OK ${String(results.length)} statements executed`);
216
+ (0, output_1.emit)(lines.join("\n"));
144
217
  }
145
218
  function dmlVerb(type) {
146
219
  switch (type) {
147
220
  case "INSERT": return "inserted";
148
221
  case "UPDATE": return "updated";
149
222
  case "DELETE": return "deleted";
223
+ case "MERGE": return "merged";
224
+ case "DML": return "affected"; // 未识别子类的兜底
150
225
  }
151
226
  }
152
227
  /** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
@@ -66,7 +66,8 @@ function resolveQueryRouting(opts) {
66
66
  }
67
67
  async function handleFileLs(opts) {
68
68
  const appId = (0, shared_1.resolveAppId)(opts);
69
- const limit = opts.limit ? Number(opts.limit) : undefined;
69
+ // commander 已经把 --limit 解析为 number;保留 ?? undefined 兼容老调用
70
+ const limit = opts.limit;
70
71
  const sizeGt = opts.sizeGt ? (0, render_1.parseSize)(opts.sizeGt) : undefined;
71
72
  const sizeLt = opts.sizeLt ? (0, render_1.parseSize)(opts.sizeLt) : undefined;
72
73
  const { path, name } = resolveQueryRouting(opts);
@@ -44,7 +44,7 @@ const shared_1 = require("../../../cli/commands/shared");
44
44
  const index_1 = require("../../../api/file/index");
45
45
  const render_1 = require("../../../utils/render");
46
46
  const node_readline_1 = __importDefault(require("node:readline"));
47
- const MAX_BATCH = 1000;
47
+ const MAX_BATCH = 100;
48
48
  /**
49
49
  * 解析位置参数(自动识别 path / file_name)与 `--name`(强制 file_name)两类输入。
50
50
  *
@@ -103,13 +103,20 @@ async function resolveDeleteInputs(appId, paths, names) {
103
103
  }
104
104
  return { resolved, errors };
105
105
  }
106
- async function confirm(count) {
106
+ /**
107
+ * 删除前 TTY 二次确认。
108
+ * PRD 单文件场景下提示带具体路径:`? Delete '/path'? (y/N)`,
109
+ * 多文件场景汇总条数:`? Delete N files? (y/N)`。
110
+ * `firstInput` 是用户传入的第一个值(可能是 path 或 file_name),
111
+ * 单文件时直接展示给用户,方便核对目标。
112
+ */
113
+ async function confirm(count, firstInput) {
107
114
  const rl = node_readline_1.default.createInterface({
108
115
  input: process.stdin,
109
116
  output: process.stderr,
110
117
  });
111
118
  return new Promise((resolve) => {
112
- const prompt = count === 1 ? `? Delete 1 file? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
119
+ const prompt = count === 1 ? `? Delete '${firstInput}'? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
113
120
  rl.question(prompt, (answer) => {
114
121
  rl.close();
115
122
  resolve(answer.trim().toLowerCase() === "y");
@@ -123,13 +130,16 @@ async function handleFileRm(paths, opts) {
123
130
  throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
124
131
  }
125
132
  if (totalCount > MAX_BATCH) {
126
- throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 1000 limit`);
133
+ throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
127
134
  }
128
135
  const appId = (0, shared_1.resolveAppId)(opts);
129
136
  // destructive guardrail
130
137
  const tty = (0, render_1.isStdoutTty)();
131
138
  if (tty && !opts.yes) {
132
- const ok = await confirm(totalCount);
139
+ // 单文件提示带具体目标方便用户核对;多文件传第一个 input 占位(实际只显示 N files)
140
+ // 上面已校验 totalCount > 0,paths/names 至少有一个非空
141
+ const firstInput = paths.length > 0 ? paths[0] : names[0];
142
+ const ok = await confirm(totalCount, firstInput);
133
143
  if (!ok) {
134
144
  throw new error_1.AppError("DESTRUCTIVE_CANCELLED", "Cancelled by user");
135
145
  }
@@ -173,6 +183,7 @@ async function handleFileRm(paths, opts) {
173
183
  input,
174
184
  code: "INTERNAL_ERROR",
175
185
  message: "Delete request returned success but file still exists (server-side issue)",
186
+ hint: undefined,
176
187
  };
177
188
  }
178
189
  catch (err) {
@@ -181,12 +192,14 @@ async function handleFileRm(paths, opts) {
181
192
  input,
182
193
  code: "FILE_NOT_FOUND",
183
194
  message: `File '${input}' does not exist at delete time`,
195
+ hint: "Run `miaoda file ls` to see available files.",
184
196
  };
185
197
  }
186
198
  return {
187
199
  input,
188
200
  code: "INTERNAL_ERROR",
189
201
  message: err instanceof Error ? err.message : "verification failed",
202
+ hint: undefined,
190
203
  };
191
204
  }
192
205
  }));
@@ -194,7 +207,9 @@ async function handleFileRm(paths, opts) {
194
207
  results.push({
195
208
  status: "error",
196
209
  input: c.input,
197
- error: { code: c.code, message: c.message },
210
+ error: c.hint
211
+ ? { code: c.code, message: c.message, hint: c.hint }
212
+ : { code: c.code, message: c.message },
198
213
  });
199
214
  }
200
215
  }
@@ -233,7 +248,12 @@ async function handleFileRm(paths, opts) {
233
248
  else
234
249
  lines.push(`FAIL\t${r.input}\t${r.error.message}`);
235
250
  }
236
- lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
251
+ if (failCount === 0) {
252
+ lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
253
+ }
254
+ else {
255
+ lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
256
+ }
237
257
  (0, output_1.emit)(lines.join("\n"));
238
258
  }
239
259
  // 退出码:任一失败 → 1;全成功 → 0
@@ -5,12 +5,14 @@ class AppError extends Error {
5
5
  code;
6
6
  retryable;
7
7
  next_actions;
8
+ statement_index;
8
9
  constructor(code, message, opts) {
9
10
  super(message);
10
11
  this.name = "AppError";
11
12
  this.code = code;
12
13
  this.retryable = opts?.retryable ?? false;
13
14
  this.next_actions = opts?.next_actions ?? [];
15
+ this.statement_index = opts?.statement_index;
14
16
  }
15
17
  toJSON() {
16
18
  return {
@@ -18,6 +20,7 @@ class AppError extends Error {
18
20
  message: this.message,
19
21
  retryable: this.retryable,
20
22
  next_actions: this.next_actions,
23
+ statement_index: this.statement_index,
21
24
  };
22
25
  }
23
26
  }
@@ -32,19 +32,22 @@ function emit(data) {
32
32
  /**
33
33
  * 输出错误(写入 stderr)
34
34
  * - pretty 模式:Error: message\n hint: ...
35
- * - json 模式:{"error_code": "...", "message": "...", "hint": "..."}
35
+ * - json 模式:PRD 约定 envelope 为 `{error: {code, message, hint?}}`
36
36
  */
37
37
  function emitError(err) {
38
38
  const info = toErrorInfo(err);
39
39
  if (isJsonMode()) {
40
- const jsonErr = {
41
- error_code: info.code,
40
+ const errObj = {
41
+ code: info.code,
42
42
  message: info.message,
43
43
  };
44
44
  if (info.next_actions && info.next_actions.length > 0) {
45
- jsonErr.hint = info.next_actions.join(" ");
45
+ errObj.hint = info.next_actions.join(" ");
46
46
  }
47
- process.stderr.write(JSON.stringify(jsonErr) + "\n");
47
+ if (typeof info.statement_index === "number") {
48
+ errObj.statement_index = info.statement_index;
49
+ }
50
+ process.stderr.write(JSON.stringify({ error: errObj }) + "\n");
48
51
  }
49
52
  else {
50
53
  process.stderr.write(`Error: ${info.message}\n`);
@@ -71,9 +71,39 @@ function formatTime(iso, isTty) {
71
71
  * 算进列宽(比如 dim "NULL" 实际占 4 字符但 .length=11),导致表格对齐错位。
72
72
  */
73
73
  const ANSI_SGR_RE = /\[[0-9;]*m/g;
74
- /** cell 可见字符宽度:剥离 ANSI 转义后再算长度。 */
74
+ /**
75
+ * 单字符的终端列宽:CJK 全角字符(汉字/假名/全角符号等)占 2 列,其它占 1 列。
76
+ * 对齐 Unicode East Asian Width 的 W/F 类,等价于 wcwidth 简化版。
77
+ * 不实现合字 / 零宽字符(ZWJ / 变体选择符)等极端情况,CLI 表格场景够用。
78
+ */
79
+ function charWidth(cp) {
80
+ if ((cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
81
+ cp === 0x2329 || cp === 0x232A ||
82
+ (cp >= 0x2E80 && cp <= 0x303E) || // CJK Radicals / Punctuation
83
+ (cp >= 0x3041 && cp <= 0x33FF) || // Hiragana / Katakana / CJK Symbols
84
+ (cp >= 0x3400 && cp <= 0x4DBF) || // CJK Ext A
85
+ (cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified
86
+ (cp >= 0xA000 && cp <= 0xA4CF) || // Yi
87
+ (cp >= 0xAC00 && cp <= 0xD7A3) || // Hangul Syllables
88
+ (cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compat Ideographs
89
+ (cp >= 0xFE30 && cp <= 0xFE4F) || // CJK Compat Forms
90
+ (cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms
91
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
92
+ (cp >= 0x20000 && cp <= 0x2FFFD) || // CJK Ext B-F
93
+ (cp >= 0x30000 && cp <= 0x3FFFD) // CJK Ext G-H
94
+ ) {
95
+ return 2;
96
+ }
97
+ return 1;
98
+ }
99
+ /** cell 可见字符宽度:剥离 ANSI 转义后按终端列宽逐字符累加(CJK 算 2 列)。 */
75
100
  function visibleWidth(s) {
76
- return s.replace(ANSI_SGR_RE, "").length;
101
+ const stripped = s.replace(ANSI_SGR_RE, "");
102
+ let w = 0;
103
+ for (const c of stripped) {
104
+ w += charWidth(c.codePointAt(0) ?? 0);
105
+ }
106
+ return w;
77
107
  }
78
108
  function padVisibleEnd(s, targetWidth) {
79
109
  const w = visibleWidth(s);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.0-alpha.8825a76",
3
+ "version": "0.1.0-alpha.8dcc262",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,21 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.traceRequest = traceRequest;
4
- exports.traceResponse = traceResponse;
5
- /**
6
- * ⚠️ TODO(REMOVE-BEFORE-RELEASE): 调试用 HTTP trace。
7
- *
8
- * 仅在 `--verbose` 下把 db 域 admin-inner 请求的 URL 与后端 logid 打到 stderr,
9
- * 方便联调时把 logid 贴给后端查链路。正式版本前请整文件删除,并把
10
- * `src/api/db/{api,client}.ts` 里的 `traceRequest()` / `traceResponse()` 调用一并移除。
11
- */
12
- const logger_1 = require("../../utils/logger");
13
- /** 请求前打 method + 完整 URL(不含敏感 header)。 */
14
- function traceRequest(method, url) {
15
- (0, logger_1.debug)(`http → ${method} ${url}`);
16
- }
17
- /** 响应后打 status + x-tt-logid(后端追踪 ID)。 */
18
- function traceResponse(method, url, status, headers) {
19
- const logId = headers?.get("x-tt-logid") ?? headers?.get("X-Tt-Logid") ?? "(none)";
20
- (0, logger_1.debug)(`http ← ${method} ${url} status=${String(status)} x-tt-logid=${logId}`);
21
- }
@@ -1,21 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.traceRequest = traceRequest;
4
- exports.traceResponse = traceResponse;
5
- /**
6
- * ⚠️ TODO(REMOVE-BEFORE-RELEASE): 调试用 HTTP trace。
7
- *
8
- * 仅在 `--verbose` 下把 file 域请求的 URL 与后端 logid 打到 stderr,
9
- * 方便联调时把 logid 贴给后端查链路。正式版本前请整文件删除,并把
10
- * `src/api/file/client.ts` 里的 `traceRequest()` / `traceResponse()` 调用一并移除。
11
- */
12
- const logger_1 = require("../../utils/logger");
13
- /** 请求前打 method + 完整 URL(不含敏感 header)。 */
14
- function traceRequest(method, url) {
15
- (0, logger_1.debug)(`http → ${method} ${url}`);
16
- }
17
- /** 响应后打 status + x-tt-logid(后端追踪 ID)。 */
18
- function traceResponse(method, url, status, headers) {
19
- const logId = headers?.get("x-tt-logid") ?? headers?.get("X-Tt-Logid") ?? "(none)";
20
- (0, logger_1.debug)(`http ← ${method} ${url} status=${String(status)} x-tt-logid=${logId}`);
21
- }