@lark-apaas/miaoda-cli 0.1.0-alpha.41ce8f5 → 0.1.0-alpha.465bdb8

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,16 +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
- let response;
30
- try {
31
- (0, _debug_trace_1.traceRequest)("POST", url, client);
32
- response = await client.post(url, { sql: opts.sql });
33
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
34
- }
35
- catch (err) {
36
- (0, _debug_trace_1.traceError)("POST", url, err, client);
37
- throw err;
38
- }
27
+ const response = await client.post(url, { sql: opts.sql });
39
28
  if (!response.ok) {
40
29
  // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
41
30
  let body = null;
@@ -68,16 +57,7 @@ async function getSchema(opts) {
68
57
  includeStats: opts.includeStats ? "true" : undefined,
69
58
  dbBranch: opts.dbBranch,
70
59
  });
71
- let response;
72
- try {
73
- (0, _debug_trace_1.traceRequest)("GET", url, client);
74
- response = await client.get(url);
75
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers, client);
76
- }
77
- catch (err) {
78
- (0, _debug_trace_1.traceError)("GET", url, err, client);
79
- throw err;
80
- }
60
+ const response = await client.get(url);
81
61
  if (!response.ok) {
82
62
  let body = null;
83
63
  try {
@@ -109,21 +89,12 @@ async function importData(opts) {
109
89
  });
110
90
  const contentType = opts.format === "csv" ? "text/csv" : "application/json";
111
91
  const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
112
- let response;
113
- try {
114
- (0, _debug_trace_1.traceRequest)("POST", url, client);
115
- response = await client.request({
116
- method: "POST",
117
- url,
118
- headers: { "Content-Type": contentType },
119
- body: ab,
120
- });
121
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
122
- }
123
- catch (err) {
124
- (0, _debug_trace_1.traceError)("POST", url, err, client);
125
- throw err;
126
- }
92
+ const response = await client.request({
93
+ method: "POST",
94
+ url,
95
+ headers: { "Content-Type": contentType },
96
+ body: ab,
97
+ });
127
98
  if (!response.ok) {
128
99
  let body = null;
129
100
  try {
@@ -160,16 +131,7 @@ async function exportData(opts) {
160
131
  dbBranch: opts.dbBranch,
161
132
  });
162
133
  const reqBody = { limit: opts.limit ?? 5000 };
163
- let response;
164
- try {
165
- (0, _debug_trace_1.traceRequest)("POST", url, client);
166
- response = await client.post(url, reqBody);
167
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
168
- }
169
- catch (err) {
170
- (0, _debug_trace_1.traceError)("POST", url, err, client);
171
- throw err;
172
- }
134
+ const response = await client.post(url, reqBody);
173
135
  if (!response.ok) {
174
136
  // 错误路径:body 是 JSON envelope
175
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,18 +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, client);
137
134
  const response = await client.get(url);
138
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers, client);
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, client);
144
- }
145
- else {
146
- (0, _debug_trace_1.traceError)("GET", url, err, client);
147
- }
148
138
  await mapHttpError(err, opts);
149
139
  throw err; // 不可达,mapHttpError 必定 throw
150
140
  }
@@ -152,18 +142,10 @@ async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
152
142
  /** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
153
143
  async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
154
144
  try {
155
- (0, _debug_trace_1.traceRequest)("POST", url, client);
156
145
  const response = await client.post(url, body);
157
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
158
146
  return (await response.json());
159
147
  }
160
148
  catch (err) {
161
- if (err instanceof http_client_1.HttpError && err.response) {
162
- (0, _debug_trace_1.traceResponse)("POST", url, err.response.status, err.response.headers, client);
163
- }
164
- else {
165
- (0, _debug_trace_1.traceError)("POST", url, err, client);
166
- }
167
149
  await mapHttpError(err, opts);
168
150
  throw err;
169
151
  }
@@ -171,18 +153,10 @@ async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()
171
153
  /** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
172
154
  async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
173
155
  try {
174
- (0, _debug_trace_1.traceRequest)(cfg.method, cfg.url, client);
175
156
  const response = await client.request(cfg);
176
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, response.status, response.headers, client);
177
157
  return (await response.json());
178
158
  }
179
159
  catch (err) {
180
- if (err instanceof http_client_1.HttpError && err.response) {
181
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, err.response.status, err.response.headers, client);
182
- }
183
- else {
184
- (0, _debug_trace_1.traceError)(cfg.method, cfg.url, err, client);
185
- }
186
160
  await mapHttpError(err, opts);
187
161
  throw err;
188
162
  }
@@ -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) => [
@@ -47,7 +47,8 @@ const index_1 = require("../../../api/db/index");
47
47
  * - --env 透传给后端 admin-inner(dbBranch 参数),不传时由后端按 workspace
48
48
  * 多环境状态兜底(多环境 → dev / 单环境 → main);后端检测到环境不存在
49
49
  * 会返 k_dl_1600000 + "Invalid DB Branch:...",CLI 侧映射为 MULTI_ENV_NOT_INITIALIZED
50
- * - 多条语句时只展示最后一条的结果(与 PRD 对齐);所有为 DDL 时展示批量摘要
50
+ * - 多语句行为对齐 PRD:每条 statement 一个独立结果元素,pretty 逐条 +
51
+ * 末尾汇总,--json 输出 data 数组。
51
52
  */
52
53
  async function handleDbSql(query, opts) {
53
54
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -65,22 +66,16 @@ async function handleDbSql(query, opts) {
65
66
  (0, output_1.emit)("✓ No results");
66
67
  return;
67
68
  }
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`);
69
+ if (results.length === 1) {
70
+ renderSingle(results[0]);
71
+ return;
72
+ }
73
+ // 多语句:每条 statement 独立结果
74
+ if ((0, output_1.isJsonMode)()) {
75
+ (0, output_1.emit)({ data: results.map((r) => toMultiElement((0, index_1.parseSqlResult)(r))) });
79
76
  return;
80
77
  }
81
- // 其他场景:展示最后一条
82
- const last = results[results.length - 1];
83
- renderSingle(last);
78
+ renderMultiPretty(results);
84
79
  }
85
80
  /** 读取 stdin 并返回完整 SQL 文本(stdin 不是 TTY 即认为被 pipe)。 */
86
81
  async function readSql(inline) {
@@ -130,23 +125,78 @@ function renderSingle(raw) {
130
125
  }
131
126
  function toJson(parsed) {
132
127
  if (parsed.kind === "select") {
128
+ // PRD 单 SELECT:data 直接是行数组
133
129
  return { data: parsed.rows };
134
130
  }
135
131
  if (parsed.kind === "dml") {
132
+ // PRD 单 DML:data = {command, rows_affected}
136
133
  return {
137
134
  data: {
138
- sql_type: parsed.sqlType.toLowerCase(),
139
- affected_rows: parsed.affectedRows,
135
+ command: parsed.sqlType,
136
+ rows_affected: parsed.affectedRows,
140
137
  },
141
138
  };
142
139
  }
143
- return { data: { sql_type: "ddl" } };
140
+ // PRD 单 DDL:data = {command, target?}。command 直接用后端给的细粒度
141
+ // (CREATE_TABLE / DROP_TABLE / ...),target 待后端给对象名后再加。
142
+ return { data: { command: parsed.sqlType } };
143
+ }
144
+ /**
145
+ * 多语句 --json 元素:与单 DDL/DML 形状一致,但 SELECT 包成
146
+ * {command:"SELECT", rows:[...]}(PRD 约定,避免数组里嵌套数组造成歧义)。
147
+ */
148
+ function toMultiElement(parsed) {
149
+ if (parsed.kind === "select") {
150
+ return { command: "SELECT", rows: parsed.rows };
151
+ }
152
+ if (parsed.kind === "dml") {
153
+ return { command: parsed.sqlType, rows_affected: parsed.affectedRows };
154
+ }
155
+ // DDL:用后端给的细粒度 command
156
+ return { command: parsed.sqlType };
157
+ }
158
+ /** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
159
+ function renderMultiPretty(results) {
160
+ const tty = (0, render_1.isStdoutTty)();
161
+ const lines = [];
162
+ for (let i = 0; i < results.length; i++) {
163
+ const parsed = (0, index_1.parseSqlResult)(results[i]);
164
+ const idx = i + 1;
165
+ if (parsed.kind === "select") {
166
+ const n = parsed.rows.length;
167
+ lines.push(`Statement ${String(idx)}: SELECT (${String(n)} row${n === 1 ? "" : "s"})`);
168
+ if (n > 0) {
169
+ const cols = collectColumns(parsed.rows);
170
+ const tbl = parsed.rows.map((r) => cols.map((c) => formatCell(r[c], tty)));
171
+ lines.push(tty ? (0, render_1.renderAlignedTable)(cols, tbl) : (0, render_1.renderTsv)(cols, tbl));
172
+ }
173
+ // 块间空行(最后一条不留)
174
+ if (i < results.length - 1)
175
+ lines.push("");
176
+ continue;
177
+ }
178
+ if (parsed.kind === "dml") {
179
+ const verb = dmlVerb(parsed.sqlType);
180
+ const n = parsed.affectedRows;
181
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}${String(n)} row${n === 1 ? "" : "s"} ${verb}`);
182
+ continue;
183
+ }
184
+ // DDL
185
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}DDL executed`);
186
+ }
187
+ // 汇总行:所有 statement 都跑完了
188
+ lines.push(tty
189
+ ? `✓ ${String(results.length)} statements executed`
190
+ : `OK ${String(results.length)} statements executed`);
191
+ (0, output_1.emit)(lines.join("\n"));
144
192
  }
145
193
  function dmlVerb(type) {
146
194
  switch (type) {
147
195
  case "INSERT": return "inserted";
148
196
  case "UPDATE": return "updated";
149
197
  case "DELETE": return "deleted";
198
+ case "MERGE": return "merged";
199
+ case "DML": return "affected"; // 未识别子类的兜底
150
200
  }
151
201
  }
152
202
  /** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
@@ -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");
@@ -129,7 +136,10 @@ async function handleFileRm(paths, 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.41ce8f5",
3
+ "version": "0.1.0-alpha.465bdb8",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,83 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.traceRequest = traceRequest;
4
- exports.traceResponse = traceResponse;
5
- exports.traceError = traceError;
6
- const config_1 = require("../../utils/config");
7
- const logger_1 = require("../../utils/logger");
8
- /**
9
- * 从 HttpClient 实例抠出 baseURL,用于把相对 path 拼成完整 URL 输出。
10
- * defaultConfig.baseURL 在 http-client 0.1.4 d.ts 里没声明但运行时可访问;
11
- * 找不到时返回空串(trace 仍能输出 path,只是不完整)。
12
- */
13
- function clientBaseURL(client) {
14
- return client?.defaultConfig?.baseURL ?? "";
15
- }
16
- /**
17
- * 给 client 注册一个最后的 request interceptor:等 platform plugin / canary
18
- * 等其它拦截器把 JWT / AK/SK / X-Miaoda-Client-Token / x-tt-env 注入 cfg.headers
19
- * 之后,用注入完毕的 cfg 拼一行可直接复制运行的 curl 命令打到 stderr。
20
- *
21
- * 拦截器只装一次(用 client 实例的 Symbol 标记),verbose 关闭时拦截器内
22
- * 直接 return(零开销)。
23
- */
24
- function installCurlTrace(client) {
25
- const c = client;
26
- if (c.__miaoda_curl_trace_installed__)
27
- return;
28
- // 单测里的 mock client 没有 interceptors,直接 noop 避免炸测试。
29
- const interceptors = client.interceptors;
30
- if (!interceptors)
31
- return;
32
- c.__miaoda_curl_trace_installed__ = true;
33
- interceptors.request.use((cfg) => {
34
- if (!(0, config_1.getConfig)().verbose)
35
- return cfg;
36
- const method = (cfg.method ?? "GET").toUpperCase();
37
- const url = `${cfg.baseURL ?? ""}${cfg.url ?? ""}`;
38
- const parts = [`curl -i -X ${method} '${url}'`];
39
- const headers = cfg.headers instanceof Headers
40
- ? cfg.headers
41
- : new Headers(cfg.headers ?? {});
42
- headers.forEach((v, k) => parts.push(` -H '${k}: ${v}'`));
43
- if (cfg.body !== undefined && cfg.body !== null) {
44
- let body;
45
- if (typeof cfg.body === "string")
46
- body = cfg.body;
47
- else if (cfg.body instanceof ArrayBuffer || cfg.body instanceof Uint8Array)
48
- body = "<binary>";
49
- else
50
- body = JSON.stringify(cfg.body);
51
- const escaped = body.replace(/'/g, "'\\''");
52
- parts.push(` -d '${escaped}'`);
53
- }
54
- (0, logger_1.debug)(`curl:\n${parts.join(" \\\n")}`);
55
- return cfg;
56
- });
57
- }
58
- /** 请求前打 method + 完整 URL;同时懒装 curl-trace 拦截器(idempotent)。 */
59
- function traceRequest(method, url, client) {
60
- if (client)
61
- installCurlTrace(client);
62
- (0, logger_1.debug)(`http → ${method} ${clientBaseURL(client)}${url}`);
63
- }
64
- /** 响应后打 status + x-tt-logid。 */
65
- function traceResponse(method, url, status, headers, client) {
66
- const logId = headers?.get("x-tt-logid") ?? headers?.get("X-Tt-Logid") ?? "(none)";
67
- (0, logger_1.debug)(`http ← ${method} ${clientBaseURL(client)}${url} status=${String(status)} x-tt-logid=${logId}`);
68
- }
69
- /**
70
- * 网络层错误(fetch failed / DNS / TLS / 连接拒绝)兜底:response 没回来,
71
- * 没有 logid,但要把错误原因记到 trace,避免用户只看到 "Error: fetch failed"。
72
- */
73
- function traceError(method, url, err, client) {
74
- const msg = err instanceof Error ? err.message : String(err);
75
- // undici 的 "fetch failed" 真实原因在 err.cause(DNS/TLS/proxy/ECONNREFUSED 各种),
76
- // 单看 message 全是 "fetch failed",把 cause 也打出来才能定位。
77
- const cause = err?.cause;
78
- const causeMsg = cause instanceof Error
79
- ? `${cause.name}: ${cause.message}${cause.code ? ` [${cause.code ?? ""}]` : ""}`
80
- : cause !== undefined ? JSON.stringify(cause) : "";
81
- const tail = causeMsg ? `, cause=${causeMsg}` : "";
82
- (0, logger_1.debug)(`http ✗ ${method} ${clientBaseURL(client)}${url} (no response, network-level error: ${msg}${tail})`);
83
- }
@@ -1,70 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.traceRequest = traceRequest;
4
- exports.traceResponse = traceResponse;
5
- exports.traceError = traceError;
6
- const config_1 = require("../../utils/config");
7
- const logger_1 = require("../../utils/logger");
8
- function clientBaseURL(client) {
9
- return client?.defaultConfig?.baseURL ?? "";
10
- }
11
- function installCurlTrace(client) {
12
- const c = client;
13
- if (c.__miaoda_curl_trace_installed__)
14
- return;
15
- // 单测里的 mock client 没有 interceptors,直接 noop 避免炸测试。
16
- const interceptors = client.interceptors;
17
- if (!interceptors)
18
- return;
19
- c.__miaoda_curl_trace_installed__ = true;
20
- interceptors.request.use((cfg) => {
21
- if (!(0, config_1.getConfig)().verbose)
22
- return cfg;
23
- const method = (cfg.method ?? "GET").toUpperCase();
24
- const url = `${cfg.baseURL ?? ""}${cfg.url ?? ""}`;
25
- const parts = [`curl -i -X ${method} '${url}'`];
26
- const headers = cfg.headers instanceof Headers
27
- ? cfg.headers
28
- : new Headers(cfg.headers ?? {});
29
- headers.forEach((v, k) => parts.push(` -H '${k}: ${v}'`));
30
- if (cfg.body !== undefined && cfg.body !== null) {
31
- let body;
32
- if (typeof cfg.body === "string")
33
- body = cfg.body;
34
- else if (cfg.body instanceof ArrayBuffer || cfg.body instanceof Uint8Array)
35
- body = "<binary>";
36
- else
37
- body = JSON.stringify(cfg.body);
38
- const escaped = body.replace(/'/g, "'\\''");
39
- parts.push(` -d '${escaped}'`);
40
- }
41
- (0, logger_1.debug)(`curl:\n${parts.join(" \\\n")}`);
42
- return cfg;
43
- });
44
- }
45
- /** 请求前打 method + 完整 URL;同时懒装 curl-trace 拦截器(idempotent)。 */
46
- function traceRequest(method, url, client) {
47
- if (client)
48
- installCurlTrace(client);
49
- (0, logger_1.debug)(`http → ${method} ${clientBaseURL(client)}${url}`);
50
- }
51
- /** 响应后打 status + x-tt-logid。 */
52
- function traceResponse(method, url, status, headers, client) {
53
- const logId = headers?.get("x-tt-logid") ?? headers?.get("X-Tt-Logid") ?? "(none)";
54
- (0, logger_1.debug)(`http ← ${method} ${clientBaseURL(client)}${url} status=${String(status)} x-tt-logid=${logId}`);
55
- }
56
- /**
57
- * 网络层错误(fetch failed / DNS / TLS / 连接拒绝)兜底:response 没回来,
58
- * 没有 logid,但要把错误原因记到 trace,避免用户只看到 "Error: fetch failed"。
59
- */
60
- function traceError(method, url, err, client) {
61
- const msg = err instanceof Error ? err.message : String(err);
62
- // undici 的 "fetch failed" 真实原因在 err.cause(DNS/TLS/proxy/ECONNREFUSED 各种),
63
- // 单看 message 全是 "fetch failed",把 cause 也打出来才能定位。
64
- const cause = err?.cause;
65
- const causeMsg = cause instanceof Error
66
- ? `${cause.name}: ${cause.message}${cause.code ? ` [${cause.code ?? ""}]` : ""}`
67
- : cause !== undefined ? JSON.stringify(cause) : "";
68
- const tail = causeMsg ? `, cause=${causeMsg}` : "";
69
- (0, logger_1.debug)(`http ✗ ${method} ${clientBaseURL(client)}${url} (no response, network-level error: ${msg}${tail})`);
70
- }