@lark-apaas/miaoda-cli 0.1.1 → 0.1.2-alpha.b2b5ae5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/app/api.js +25 -0
- package/dist/api/app/index.js +15 -0
- package/dist/api/app/schemas.js +79 -0
- package/dist/api/app/types.js +58 -0
- package/dist/api/db/api.js +83 -6
- package/dist/api/db/client.js +40 -29
- package/dist/api/db/parsers.js +33 -20
- package/dist/api/db/sql-keywords.js +123 -0
- package/dist/api/deploy/api.js +60 -0
- package/dist/api/deploy/index.js +16 -0
- package/dist/api/deploy/schemas.js +103 -0
- package/dist/api/deploy/types.js +22 -0
- package/dist/api/file/api.js +78 -24
- package/dist/api/file/client.js +1 -5
- package/dist/api/file/parsers.js +1 -5
- package/dist/api/index.js +7 -1
- package/dist/api/observability/api.js +52 -0
- package/dist/api/observability/index.js +16 -0
- package/dist/api/observability/schemas.js +39 -0
- package/dist/api/observability/types.js +27 -0
- package/dist/api/plugin/api.js +8 -3
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +1 -0
- package/dist/cli/commands/deploy/index.js +139 -0
- package/dist/cli/commands/index.js +6 -0
- package/dist/cli/commands/observability/index.js +227 -0
- package/dist/cli/commands/plugin/index.js +18 -6
- package/dist/cli/commands/shared.js +38 -6
- package/dist/cli/handlers/app/get.js +48 -0
- package/dist/cli/handlers/app/index.js +7 -0
- package/dist/cli/handlers/app/update.js +59 -0
- package/dist/cli/handlers/db/data.js +22 -2
- package/dist/cli/handlers/db/schema.js +22 -8
- package/dist/cli/handlers/db/sql.js +304 -16
- package/dist/cli/handlers/deploy/deploy.js +83 -0
- package/dist/cli/handlers/deploy/error-log.js +61 -0
- package/dist/cli/handlers/deploy/get.js +70 -0
- package/dist/cli/handlers/deploy/helpers.js +41 -0
- package/dist/cli/handlers/deploy/history.js +70 -0
- package/dist/cli/handlers/deploy/index.js +14 -0
- package/dist/cli/handlers/deploy/polling.js +139 -0
- package/dist/cli/handlers/file/cp.js +39 -17
- package/dist/cli/handlers/file/ls.js +1 -3
- package/dist/cli/handlers/file/rm.js +4 -3
- package/dist/cli/handlers/observability/analytics.js +189 -0
- package/dist/cli/handlers/observability/helpers.js +66 -0
- package/dist/cli/handlers/observability/index.js +12 -0
- package/dist/cli/handlers/observability/log.js +94 -0
- package/dist/cli/handlers/observability/metric.js +208 -0
- package/dist/cli/handlers/observability/trace.js +102 -0
- package/dist/cli/handlers/plugin/plugin-local.js +23 -9
- package/dist/cli/handlers/plugin/plugin.js +21 -7
- package/dist/cli/help.js +5 -2
- package/dist/utils/colors.js +98 -0
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/error.js +11 -0
- package/dist/utils/fuzzy-match.js +91 -0
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +32 -0
- package/dist/utils/index.js +13 -1
- package/dist/utils/output.js +397 -12
- package/dist/utils/render.js +61 -41
- package/dist/utils/time.js +132 -0
- package/package.json +16 -6
|
@@ -56,7 +56,9 @@ async function handleDbDataImport(file, opts) {
|
|
|
56
56
|
catch (err) {
|
|
57
57
|
const code = err.code;
|
|
58
58
|
if (code === "ENOENT") {
|
|
59
|
-
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
59
|
+
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
60
|
+
next_actions: ["Check the file path."],
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
throw err;
|
|
62
64
|
}
|
|
@@ -100,6 +102,20 @@ async function handleDbDataExport(table, opts) {
|
|
|
100
102
|
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_ROWS) {
|
|
101
103
|
throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer ≤ ${String(MAX_ROWS)}`);
|
|
102
104
|
}
|
|
105
|
+
if (!opts.force) {
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(outputPath);
|
|
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
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err instanceof error_1.AppError)
|
|
114
|
+
throw err;
|
|
115
|
+
if (err.code !== "ENOENT")
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
103
119
|
const result = await api.db.exportData({
|
|
104
120
|
appId,
|
|
105
121
|
tableName: table,
|
|
@@ -107,7 +123,11 @@ async function handleDbDataExport(table, opts) {
|
|
|
107
123
|
limit,
|
|
108
124
|
});
|
|
109
125
|
if (result.body.length > MAX_SIZE_BYTES) {
|
|
110
|
-
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
126
|
+
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
127
|
+
next_actions: [
|
|
128
|
+
`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`,
|
|
129
|
+
],
|
|
130
|
+
});
|
|
111
131
|
}
|
|
112
132
|
await fs.writeFile(outputPath, result.body);
|
|
113
133
|
// 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
|
|
@@ -40,11 +40,17 @@ const error_1 = require("../../../utils/error");
|
|
|
40
40
|
const output_1 = require("../../../utils/output");
|
|
41
41
|
const render_1 = require("../../../utils/render");
|
|
42
42
|
const shared_1 = require("../../../cli/commands/shared");
|
|
43
|
+
const colors_1 = require("../../../utils/colors");
|
|
43
44
|
const index_1 = require("../../../api/db/index");
|
|
44
45
|
// ── schema list ──
|
|
45
46
|
async function handleDbSchemaList(opts) {
|
|
46
47
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
47
|
-
const resp = await api.db.getSchema({
|
|
48
|
+
const resp = await api.db.getSchema({
|
|
49
|
+
appId,
|
|
50
|
+
format: "schema",
|
|
51
|
+
includeStats: true,
|
|
52
|
+
dbBranch: opts.env,
|
|
53
|
+
});
|
|
48
54
|
const tables = (0, index_1.flattenSchemaList)(resp.schema);
|
|
49
55
|
if ((0, output_1.isJsonMode)()) {
|
|
50
56
|
(0, output_1.emit)({ data: tables });
|
|
@@ -68,7 +74,7 @@ async function handleDbSchemaList(opts) {
|
|
|
68
74
|
t.name,
|
|
69
75
|
t.description ?? "—",
|
|
70
76
|
t.estimated_row_count === null ? "—" : String(t.estimated_row_count),
|
|
71
|
-
t.size_bytes === null ? "—" :
|
|
77
|
+
t.size_bytes === null ? "—" : tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes),
|
|
72
78
|
String(t.columns),
|
|
73
79
|
]);
|
|
74
80
|
(0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
|
|
@@ -108,9 +114,7 @@ async function handleDbSchemaGet(table, opts) {
|
|
|
108
114
|
const detail = (0, index_1.pickTableDetail)(resp.schema, table);
|
|
109
115
|
if (!detail) {
|
|
110
116
|
throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
|
|
111
|
-
next_actions: [
|
|
112
|
-
`Did you mean another table? Run "miaoda db schema list" to see all tables.`,
|
|
113
|
-
],
|
|
117
|
+
next_actions: [`Did you mean another table? Run "miaoda db schema list" to see all tables.`],
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
if ((0, output_1.isJsonMode)()) {
|
|
@@ -127,7 +131,8 @@ function renderDetail(d, tty) {
|
|
|
127
131
|
// 构造的伪时间戳(baseTime=2020-01-01 + OID 秒偏移),仅保排序意义、绝对值
|
|
128
132
|
// 误导性强,先去掉。后续如果接 ddl_change_log 取真实时间再加回。
|
|
129
133
|
const header = [
|
|
130
|
-
|
|
134
|
+
// 表名做 cyan 强调(spec:spec 里的"命令名/表名"类强调值都走 highlight)
|
|
135
|
+
["Name", tty ? colors_1.c.highlight(d.name) : d.name],
|
|
131
136
|
["Description", d.description ?? "—"],
|
|
132
137
|
[
|
|
133
138
|
"Columns",
|
|
@@ -150,12 +155,21 @@ function renderDetail(d, tty) {
|
|
|
150
155
|
parts.push((0, render_1.renderKeyValue)(header, tty));
|
|
151
156
|
parts.push("");
|
|
152
157
|
parts.push(tty ? (0, render_1.renderAlignedTable)(colHeaders, colRows) : (0, render_1.renderTsv)(colHeaders, colRows));
|
|
158
|
+
// Constraints 段(PRIMARY KEY / UNIQUE):表级约束独立成段,与普通索引分离展示
|
|
159
|
+
if (d.constraints.length > 0) {
|
|
160
|
+
parts.push("");
|
|
161
|
+
parts.push(tty ? " Constraints:" : "Constraints:");
|
|
162
|
+
for (const c of d.constraints) {
|
|
163
|
+
const line = `${c.type} (${c.columns.join(", ")})`;
|
|
164
|
+
parts.push(tty ? ` ${line}` : line);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Indexes 段:普通索引,格式 "<name> ON <col1, col2> USING <method>"
|
|
153
168
|
if (d.indexes.length > 0) {
|
|
154
169
|
parts.push("");
|
|
155
170
|
parts.push(tty ? " Indexes:" : "Indexes:");
|
|
156
|
-
// PRD 格式: "TYPE (col1, col2)",不展示索引名
|
|
157
171
|
for (const idx of d.indexes) {
|
|
158
|
-
const line = `${idx.
|
|
172
|
+
const line = `${idx.name} ON ${idx.columns.join(", ")} USING ${idx.method}`;
|
|
159
173
|
parts.push(tty ? ` ${line}` : line);
|
|
160
174
|
}
|
|
161
175
|
}
|
|
@@ -44,7 +44,10 @@ const config_1 = require("../../../utils/config");
|
|
|
44
44
|
const logger_1 = require("../../../utils/logger");
|
|
45
45
|
const shared_1 = require("../../../cli/commands/shared");
|
|
46
46
|
const render_1 = require("../../../utils/render");
|
|
47
|
+
const colors_1 = require("../../../utils/colors");
|
|
48
|
+
const fuzzy_match_1 = require("../../../utils/fuzzy-match");
|
|
47
49
|
const index_1 = require("../../../api/db/index");
|
|
50
|
+
const sql_keywords_1 = require("../../../api/db/sql-keywords");
|
|
48
51
|
const node_child_process_1 = require("node:child_process");
|
|
49
52
|
const node_fs_1 = require("node:fs");
|
|
50
53
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -64,7 +67,20 @@ async function handleDbSql(query, opts) {
|
|
|
64
67
|
if (!sql.trim()) {
|
|
65
68
|
throw new error_1.AppError("ARGS_INVALID", "Empty SQL (no inline query and stdin is empty)");
|
|
66
69
|
}
|
|
67
|
-
|
|
70
|
+
let results;
|
|
71
|
+
try {
|
|
72
|
+
results = await api.db.execSql({ appId, sql, dbBranch: opts.env });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// 错误抛出前富化 next_actions:识别"语法错误"/"表不存在"/"列不存在"
|
|
76
|
+
// 三类常见 PG 错误,做 fuzzy match 给 did-you-mean 提示。
|
|
77
|
+
// 富化失败任何环节都静默吞掉,不破坏原始错误信息。
|
|
78
|
+
await enrichSqlError(err, { appId, env: opts.env, sql });
|
|
79
|
+
// PRD 多语句失败:把服务端给的 partial_results 转成 `completed`,推断 `rolled_back`,
|
|
80
|
+
// 并把 pretty 模式的 `Statement N: ✓ ... / Statement K: ✗ ...` 立即打到 stderr。
|
|
81
|
+
enrichMultiStatementError(err, sql);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
68
84
|
if (results.length === 0) {
|
|
69
85
|
// 后端未返回任何结果,通常不会发生
|
|
70
86
|
if ((0, output_1.isJsonMode)()) {
|
|
@@ -225,19 +241,19 @@ function renderSingle(raw) {
|
|
|
225
241
|
return;
|
|
226
242
|
}
|
|
227
243
|
const cols = collectColumns(parsed.rows);
|
|
228
|
-
const rows = parsed.rows.map((r) => cols.map((
|
|
244
|
+
const rows = parsed.rows.map((r) => cols.map((col) => formatCell(r[col], tty)));
|
|
229
245
|
(0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(cols, rows) : (0, render_1.renderTsv)(cols, rows));
|
|
230
246
|
return;
|
|
231
247
|
}
|
|
232
248
|
if (parsed.kind === "dml") {
|
|
233
249
|
const verb = dmlVerb(parsed.sqlType);
|
|
234
250
|
(0, output_1.emit)(tty
|
|
235
|
-
? `✓ ${String(parsed.affectedRows)} row${parsed.affectedRows === 1 ? "" : "s"} ${verb}`
|
|
251
|
+
? colors_1.c.success(`✓ ${String(parsed.affectedRows)} row${parsed.affectedRows === 1 ? "" : "s"} ${verb}`)
|
|
236
252
|
: `OK ${String(parsed.affectedRows)} rows ${verb}`);
|
|
237
253
|
return;
|
|
238
254
|
}
|
|
239
255
|
// DDL
|
|
240
|
-
(0, output_1.emit)(tty ? "✓ DDL executed" : "OK DDL executed");
|
|
256
|
+
(0, output_1.emit)(tty ? colors_1.c.success("✓ DDL executed") : "OK DDL executed");
|
|
241
257
|
}
|
|
242
258
|
function toJson(parsed) {
|
|
243
259
|
if (parsed.kind === "select") {
|
|
@@ -293,7 +309,10 @@ function parseJsonFields() {
|
|
|
293
309
|
const v = (0, config_1.getConfig)().json;
|
|
294
310
|
if (typeof v !== "string" || v === "")
|
|
295
311
|
return null;
|
|
296
|
-
return v
|
|
312
|
+
return v
|
|
313
|
+
.split(",")
|
|
314
|
+
.map((s) => s.trim())
|
|
315
|
+
.filter((s) => s.length > 0);
|
|
297
316
|
}
|
|
298
317
|
/** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
|
|
299
318
|
function renderMultiPretty(results) {
|
|
@@ -307,7 +326,7 @@ function renderMultiPretty(results) {
|
|
|
307
326
|
lines.push(`Statement ${String(idx)}: SELECT (${String(n)} row${n === 1 ? "" : "s"})`);
|
|
308
327
|
if (n > 0) {
|
|
309
328
|
const cols = collectColumns(parsed.rows);
|
|
310
|
-
const tbl = parsed.rows.map((r) => cols.map((
|
|
329
|
+
const tbl = parsed.rows.map((r) => cols.map((col) => formatCell(r[col], tty)));
|
|
311
330
|
lines.push(tty ? (0, render_1.renderAlignedTable)(cols, tbl) : (0, render_1.renderTsv)(cols, tbl));
|
|
312
331
|
}
|
|
313
332
|
// 块间空行(最后一条不留)
|
|
@@ -318,25 +337,31 @@ function renderMultiPretty(results) {
|
|
|
318
337
|
if (parsed.kind === "dml") {
|
|
319
338
|
const verb = dmlVerb(parsed.sqlType);
|
|
320
339
|
const n = parsed.affectedRows;
|
|
321
|
-
|
|
340
|
+
const body = `${String(n)} row${n === 1 ? "" : "s"} ${verb}`;
|
|
341
|
+
lines.push(`Statement ${String(idx)}: ${tty ? colors_1.c.success("✓ " + body) : body}`);
|
|
322
342
|
continue;
|
|
323
343
|
}
|
|
324
344
|
// DDL
|
|
325
|
-
lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : "
|
|
345
|
+
lines.push(`Statement ${String(idx)}: ${tty ? colors_1.c.success("✓ DDL executed") : "DDL executed"}`);
|
|
326
346
|
}
|
|
327
347
|
// 汇总行:所有 statement 都跑完了
|
|
328
348
|
lines.push(tty
|
|
329
|
-
? `✓ ${String(results.length)} statements executed`
|
|
349
|
+
? colors_1.c.success(`✓ ${String(results.length)} statements executed`)
|
|
330
350
|
: `OK ${String(results.length)} statements executed`);
|
|
331
351
|
(0, output_1.emit)(lines.join("\n"));
|
|
332
352
|
}
|
|
333
353
|
function dmlVerb(type) {
|
|
334
354
|
switch (type) {
|
|
335
|
-
case "INSERT":
|
|
336
|
-
|
|
337
|
-
case "
|
|
338
|
-
|
|
339
|
-
case "
|
|
355
|
+
case "INSERT":
|
|
356
|
+
return "inserted";
|
|
357
|
+
case "UPDATE":
|
|
358
|
+
return "updated";
|
|
359
|
+
case "DELETE":
|
|
360
|
+
return "deleted";
|
|
361
|
+
case "MERGE":
|
|
362
|
+
return "merged";
|
|
363
|
+
case "DML":
|
|
364
|
+
return "affected"; // 未识别子类的兜底
|
|
340
365
|
}
|
|
341
366
|
}
|
|
342
367
|
/** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
|
|
@@ -356,12 +381,275 @@ function collectColumns(rows) {
|
|
|
356
381
|
*/
|
|
357
382
|
function formatCell(v, tty) {
|
|
358
383
|
if (v === null || v === undefined) {
|
|
359
|
-
return tty ? "
|
|
384
|
+
return tty ? colors_1.c.muted("NULL") : "NULL";
|
|
360
385
|
}
|
|
361
|
-
if (typeof v === "string")
|
|
386
|
+
if (typeof v === "string") {
|
|
387
|
+
// TTY 下检测到 ISO 8601 时间字符串 → 转成相对时间("3h ago" / "2d ago" /
|
|
388
|
+
// "2026-03-15"),方便 _created_at / _updated_at 等列直观可读
|
|
389
|
+
if (tty && ISO_TIMESTAMP_RE.test(v) && !Number.isNaN(Date.parse(v))) {
|
|
390
|
+
return (0, render_1.formatTime)(v, tty);
|
|
391
|
+
}
|
|
362
392
|
return v;
|
|
393
|
+
}
|
|
363
394
|
if (typeof v === "number" || typeof v === "boolean")
|
|
364
395
|
return String(v);
|
|
365
396
|
// object / array → JSON
|
|
366
397
|
return JSON.stringify(v);
|
|
367
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* 匹配 PG / ISO 8601 形态的日期时间字符串:
|
|
401
|
+
* 2026-04-29
|
|
402
|
+
* 2026-04-29 19:07:43 (PG 默认 timestamp 形态)
|
|
403
|
+
* 2026-04-29T19:07:43
|
|
404
|
+
* 2026-04-29T19:07:43.882+08:00
|
|
405
|
+
* 2026-04-29T19:07:43.882Z
|
|
406
|
+
* 时间部分 / 毫秒 / 时区可选。
|
|
407
|
+
*/
|
|
408
|
+
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
409
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
410
|
+
// db sql 错误富化:识别 PG 常见报错形态 → fuzzy match → 加 did-you-mean hint
|
|
411
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
412
|
+
const SYNTAX_RE = /syntax error at or near "([^"]+)"/i;
|
|
413
|
+
const RELATION_RE = /relation "([^"]+)" does not exist/i;
|
|
414
|
+
const COLUMN_RE = /column "([^"]+)"(?: of relation "([^"]+)")? does not exist/i;
|
|
415
|
+
/**
|
|
416
|
+
* 在错误抛出前富化 next_actions(最多 1 条 hint),按 PG 报错形态分发:
|
|
417
|
+
*
|
|
418
|
+
* 表 / 列不存在路径:
|
|
419
|
+
* - `column "X" of relation "Y" does not exist` → 在 Y 表的列里 fuzzy
|
|
420
|
+
* - `column "X" does not exist`(未指定表) → 跨表所有列合集 fuzzy
|
|
421
|
+
* - `relation "X" does not exist` → 在所有表名里 fuzzy
|
|
422
|
+
*
|
|
423
|
+
* 语法错误路径(`syntax error at or near "X"`)三条互斥子路径:
|
|
424
|
+
* a. X 字面就是保留字(用户拿 keyword 当标识符,如 `FROM order`,
|
|
425
|
+
* 但 `order` 是 PG 保留字)→ 当作"想引用的表名",远程 schema fuzzy。
|
|
426
|
+
* **不要建议 keyword 自己**,那只是把用户输入大写返还,毫无价值。
|
|
427
|
+
* b. X 接近某 keyword(典型拼写错误,如 SELCT)→ suggest keyword
|
|
428
|
+
* c. X 是数字 / 标点(如 `LIMITT 1` 报错点到 `1`)→ fallback 扫整段
|
|
429
|
+
* SQL 找看似拼错的 keyword token
|
|
430
|
+
*
|
|
431
|
+
* 任一环节失败(pattern 不匹配 / 远程拉表失败 / 没 fuzzy 候选)都静默吞掉,
|
|
432
|
+
* 不破坏原始错误信息——原则:"只在我们高置信能帮上忙时才插嘴"。
|
|
433
|
+
*/
|
|
434
|
+
async function enrichSqlError(err, ctx) {
|
|
435
|
+
if (!(err instanceof error_1.AppError))
|
|
436
|
+
return;
|
|
437
|
+
const msg = err.message;
|
|
438
|
+
// ── 表 / 列不存在路径 ──
|
|
439
|
+
// 注意:列报错 'column "X" of relation "Y"' 也含 'relation "Y"' 子串,
|
|
440
|
+
// 会同时命中 RELATION_RE。先 colMatch(更精确),命中后不再走 relation 分支。
|
|
441
|
+
const colMatch = COLUMN_RE.exec(msg);
|
|
442
|
+
const relMatch = colMatch ? null : RELATION_RE.exec(msg);
|
|
443
|
+
if (relMatch || colMatch) {
|
|
444
|
+
const tableMap = await loadTableMap(ctx);
|
|
445
|
+
if (!tableMap)
|
|
446
|
+
return;
|
|
447
|
+
if (relMatch) {
|
|
448
|
+
enrichRelationNotExist(err, relMatch[1], tableMap);
|
|
449
|
+
}
|
|
450
|
+
else if (colMatch) {
|
|
451
|
+
enrichColumnNotExist(err, colMatch, tableMap);
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// ── 语法错误路径 ──
|
|
456
|
+
const kwMatch = SYNTAX_RE.exec(msg);
|
|
457
|
+
if (!kwMatch)
|
|
458
|
+
return;
|
|
459
|
+
const token = kwMatch[1];
|
|
460
|
+
const upper = token.toUpperCase();
|
|
461
|
+
// 子路径 a:token 字面就是 keyword → 用户拿保留字当标识符
|
|
462
|
+
if (sql_keywords_1.SQL_KEYWORDS.includes(upper)) {
|
|
463
|
+
const tableMap = await loadTableMap(ctx);
|
|
464
|
+
if (!tableMap)
|
|
465
|
+
return;
|
|
466
|
+
const guess = (0, fuzzy_match_1.suggest)(token, Object.keys(tableMap));
|
|
467
|
+
if (guess) {
|
|
468
|
+
err.next_actions.push(`Did you mean table '${guess}'? '${token}' is a reserved keyword; ` +
|
|
469
|
+
`quote it as "${token}" if you really mean a table named '${token}'.`);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// 子路径 b:token 接近 keyword(拼写错误)
|
|
474
|
+
const guess = (0, fuzzy_match_1.suggest)(upper, sql_keywords_1.SQL_KEYWORDS);
|
|
475
|
+
if (guess && guess !== upper) {
|
|
476
|
+
err.next_actions.push(`Check SQL keyword spelling. Did you mean "${guess}"?`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// 子路径 c:PG 错误位置点到了下一个 token(如 `LIMITT 1` → 报错指向 "1"),
|
|
480
|
+
// 扫整段 SQL 找看似拼错的 keyword
|
|
481
|
+
const typo = findKeywordTypo(ctx.sql);
|
|
482
|
+
if (typo) {
|
|
483
|
+
err.next_actions.push(`Check SQL keyword spelling. '${typo.input}' looks like '${typo.suggest}'.`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** 远程拉一次 schema 并扁平化为 `tableName → fieldName[]`。失败返 null(调用方静默不加 hint)。 */
|
|
487
|
+
async function loadTableMap(ctx) {
|
|
488
|
+
try {
|
|
489
|
+
const resp = await api.db.getSchema({
|
|
490
|
+
appId: ctx.appId,
|
|
491
|
+
format: "schema",
|
|
492
|
+
dbBranch: ctx.env,
|
|
493
|
+
});
|
|
494
|
+
return extractTableFieldMap(resp.schema);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function enrichRelationNotExist(err, relation, tableMap) {
|
|
501
|
+
const guess = (0, fuzzy_match_1.suggest)(relation, Object.keys(tableMap));
|
|
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.");
|
|
505
|
+
}
|
|
506
|
+
function enrichColumnNotExist(err, colMatch, tableMap) {
|
|
507
|
+
const colName = colMatch[1];
|
|
508
|
+
// TS RegExpMatchArray 数字索引类型 = string,但 optional capture group `(...)?`
|
|
509
|
+
// 没匹配时运行时返 undefined;as cast 把这个事实告诉类型系统。
|
|
510
|
+
const relation = colMatch[2];
|
|
511
|
+
const relationCols = relation !== undefined ? tableMap[relation] : undefined;
|
|
512
|
+
if (relation !== undefined && relationCols !== undefined) {
|
|
513
|
+
// 指定了表 → 在该表的列里找
|
|
514
|
+
const guess = (0, fuzzy_match_1.suggest)(colName, relationCols);
|
|
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;
|
|
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.");
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* 扫整段 SQL 找一个看似"拼错的关键字"token:纯字母 + 长度 ≥ 3 + 不是已知 keyword
|
|
530
|
+
* + fuzzy 命中某个 keyword(且距离 > 0)。返第一个命中的 token。
|
|
531
|
+
*
|
|
532
|
+
* 用于 `syntax error at or near "<X>"` 的 X 是数字 / 标点的兜底场景——例如
|
|
533
|
+
* `... LIMITT 1` 报错点到 `1`,但用户真正的拼写错误在 LIMITT。
|
|
534
|
+
*
|
|
535
|
+
* 阈值复用 suggest(),候选词越短匹配越严,避免把 `userid` / `usrname` 这种
|
|
536
|
+
* 用户标识符误抓为关键字 typo。
|
|
537
|
+
*/
|
|
538
|
+
function findKeywordTypo(sql) {
|
|
539
|
+
const tokens = sql.split(/[\s,;()'"`*=<>+\-./]+/).filter((t) => /^[A-Za-z_]{3,}$/.test(t));
|
|
540
|
+
for (const tok of tokens) {
|
|
541
|
+
const upper = tok.toUpperCase();
|
|
542
|
+
if (sql_keywords_1.SQL_KEYWORDS.includes(upper))
|
|
543
|
+
continue; // 写对的关键字
|
|
544
|
+
const guess = (0, fuzzy_match_1.suggest)(upper, sql_keywords_1.SQL_KEYWORDS);
|
|
545
|
+
if (guess && guess !== upper) {
|
|
546
|
+
return { input: tok, suggest: guess };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
/** 从 InnerSchemaRespVO 提取 `tableName → fieldName[]` 映射,扁平化 tables/views/mvs 三池。 */
|
|
552
|
+
function extractTableFieldMap(s) {
|
|
553
|
+
const out = {};
|
|
554
|
+
if (!s)
|
|
555
|
+
return out;
|
|
556
|
+
const pools = [s.tables?.data, s.views?.data, s.materializedViews?.data];
|
|
557
|
+
for (const pool of pools) {
|
|
558
|
+
if (!pool)
|
|
559
|
+
continue;
|
|
560
|
+
for (const t of pool) {
|
|
561
|
+
out[t.tableName] = (t.fields ?? []).map((f) => f.fieldName);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* 多语句 SQL 失败时把服务端透传的 partial_results 包装成 PRD 期望形态:
|
|
568
|
+
* - 用现有 toMultiElement 把每条 SQLExecuteResult 转成 `{command, ...}` 元素,
|
|
569
|
+
* 与正常 multi-statement 成功路径的 data[] 结构完全一致
|
|
570
|
+
* - 推断 rolled_back:扫 completed 数组里 BEGIN/COMMIT/ROLLBACK 计数;失败时
|
|
571
|
+
* 还在 user tx 内 ⇒ 服务端 closeUserTxIfOpen 已发 ROLLBACK,标 true
|
|
572
|
+
* - rolled_back=true 时给 next_actions 追加一句 spec 文案
|
|
573
|
+
* - pretty 模式(非 JSON)把 `Statement N: ✓ ... / K: ✗ ...` 立即打到 stderr,
|
|
574
|
+
* 与 emitError 的 Error/hint 行连成一段 PRD 期望的 multi-statement 报告
|
|
575
|
+
*/
|
|
576
|
+
function enrichMultiStatementError(err, sql) {
|
|
577
|
+
if (!(err instanceof error_1.AppError))
|
|
578
|
+
return;
|
|
579
|
+
// 用 SQL 文本判断是否多语句,而不是 partial.length——第一条就失败时
|
|
580
|
+
// partial_results=[],但仍需输出统一的多语句 envelope 字段保持格式一致。
|
|
581
|
+
const total = countStatements(sql);
|
|
582
|
+
if (total <= 1)
|
|
583
|
+
return;
|
|
584
|
+
const partial = Array.isArray(err.partial_results)
|
|
585
|
+
? err.partial_results
|
|
586
|
+
: [];
|
|
587
|
+
const completed = partial.map((r) => toMultiElement((0, index_1.parseSqlResult)(r)));
|
|
588
|
+
err.completed = completed;
|
|
589
|
+
err.rolled_back = inferRolledBack(completed);
|
|
590
|
+
err.total_statements = total;
|
|
591
|
+
// pretty 模式(非 JSON)打 per-statement breakdown 到 stderr
|
|
592
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
593
|
+
writeMultiStatementBreakdown(err, completed);
|
|
594
|
+
}
|
|
595
|
+
// rolled_back=true 时追加 spec hint:"Transaction rolled back; no changes persisted."
|
|
596
|
+
if (err.rolled_back) {
|
|
597
|
+
err.next_actions.push("Transaction rolled back; no changes persisted.");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* 推断本次多语句执行最终是否处于"事务被回滚"状态:
|
|
602
|
+
* 遍历 completed[] 数组,BEGIN +1 / COMMIT|ROLLBACK -1;
|
|
603
|
+
* 失败时 depth > 0 → 用户事务还开着 → 服务端 closeUserTxIfOpen 发了 ROLLBACK
|
|
604
|
+
* 失败时 depth = 0 → 失败语句在 autocommit 模式 → 已成功的 statement 真实落库
|
|
605
|
+
*/
|
|
606
|
+
function inferRolledBack(completed) {
|
|
607
|
+
let depth = 0;
|
|
608
|
+
for (const e of completed) {
|
|
609
|
+
if (e.command === "BEGIN")
|
|
610
|
+
depth++;
|
|
611
|
+
else if (e.command === "COMMIT" || e.command === "ROLLBACK")
|
|
612
|
+
depth--;
|
|
613
|
+
}
|
|
614
|
+
return depth > 0;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 数 SQL 里有几条独立语句:先去掉单 / 双引号字面量(防止 'a;b' 里的分号被算成
|
|
618
|
+
* 分隔符),再按分号 split,过滤空条。CLI 用户场景几乎都是简单 SQL,不处理
|
|
619
|
+
* dollar-quoted($$..$$)等高级形态——估错 ±1 不影响 hint 可读性。
|
|
620
|
+
*/
|
|
621
|
+
function countStatements(sql) {
|
|
622
|
+
const stripped = sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""');
|
|
623
|
+
return stripped.split(/;+/).filter((s) => s.trim().length > 0).length;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* 写 `Statement N: ✓ ... / Statement K: ✗ ...` 到 stderr,对齐 PRD spec 的
|
|
627
|
+
* 多语句失败 pretty 输出形态。
|
|
628
|
+
*/
|
|
629
|
+
function writeMultiStatementBreakdown(err, completed) {
|
|
630
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
631
|
+
const lines = [];
|
|
632
|
+
for (let i = 0; i < completed.length; i++) {
|
|
633
|
+
const e = completed[i];
|
|
634
|
+
const verb = e.command === "BEGIN" || e.command === "COMMIT" || e.command === "ROLLBACK"
|
|
635
|
+
? e.command
|
|
636
|
+
: describeCompleted(e);
|
|
637
|
+
lines.push(`Statement ${String(i + 1)}: ${tty ? colors_1.c.success("✓ ") : "✓ "}${verb}`);
|
|
638
|
+
}
|
|
639
|
+
// 失败那条
|
|
640
|
+
const failedIdx = err.statement_index ?? completed.length;
|
|
641
|
+
lines.push(`Statement ${String(failedIdx + 1)}: ${tty ? colors_1.c.fail("✗ ") : "✗ "}${err.message}`);
|
|
642
|
+
process.stderr.write(lines.join("\n") + "\n\n");
|
|
643
|
+
}
|
|
644
|
+
/** completed 单条结果的人类可读描述(用于 stderr breakdown 行尾)。 */
|
|
645
|
+
function describeCompleted(e) {
|
|
646
|
+
const r = e;
|
|
647
|
+
if (r.command === "SELECT" && Array.isArray(r.rows)) {
|
|
648
|
+
return `SELECT (${String(r.rows.length)} row${r.rows.length === 1 ? "" : "s"})`;
|
|
649
|
+
}
|
|
650
|
+
if (typeof r.rows_affected === "number") {
|
|
651
|
+
const verb = dmlVerb(r.command);
|
|
652
|
+
return `${String(r.rows_affected)} row${r.rows_affected === 1 ? "" : "s"} ${verb}`;
|
|
653
|
+
}
|
|
654
|
+
return `${r.command} executed`;
|
|
655
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleDeploy = handleDeploy;
|
|
37
|
+
const api = __importStar(require("../../../api/index"));
|
|
38
|
+
const output_1 = require("../../../utils/output");
|
|
39
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
40
|
+
const index_1 = require("../../../api/deploy/index");
|
|
41
|
+
const polling_1 = require("./polling");
|
|
42
|
+
/** miaoda deploy [--branch ...] [--wait] [--timeout 300] */
|
|
43
|
+
async function handleDeploy(opts) {
|
|
44
|
+
const appID = (0, shared_1.resolveAppId)({ appId: opts.appId });
|
|
45
|
+
const resp = await api.deploy.createRelease({ appID, branch: opts.branch });
|
|
46
|
+
const pipelineTaskID = resp.pipelineTaskID ?? "";
|
|
47
|
+
if (!opts.wait) {
|
|
48
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
49
|
+
process.stdout.write(`✓ Deployment triggered. (deploy-id: ${pipelineTaskID})\n`);
|
|
50
|
+
}
|
|
51
|
+
(0, output_1.emit)({
|
|
52
|
+
data: { pipelineTaskID },
|
|
53
|
+
next_cursor: null,
|
|
54
|
+
has_more: false,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
59
|
+
process.stdout.write(`Waiting for deployment to complete... (deploy-id: ${pipelineTaskID})\n`);
|
|
60
|
+
}
|
|
61
|
+
const detail = await (0, polling_1.waitForPipeline)({
|
|
62
|
+
appID,
|
|
63
|
+
pipelineTaskID,
|
|
64
|
+
timeoutSec: opts.timeout ?? 300,
|
|
65
|
+
});
|
|
66
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
67
|
+
if (detail.status === index_1.NodeStatus.SUCCESS) {
|
|
68
|
+
process.stdout.write("✓ 发布成功\n");
|
|
69
|
+
}
|
|
70
|
+
else if (detail.status === index_1.NodeStatus.FAILED) {
|
|
71
|
+
process.stdout.write("✗ 发布失败\n");
|
|
72
|
+
process.stdout.write(` hint: Run \`miaoda deploy error-log ${pipelineTaskID}\` to view error logs\n`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
process.stdout.write("• 发布已取消\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
(0, output_1.emit)({
|
|
79
|
+
data: { pipelineTaskID, detail },
|
|
80
|
+
next_cursor: null,
|
|
81
|
+
has_more: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleDeployErrorLog = handleDeployErrorLog;
|
|
37
|
+
const api = __importStar(require("../../../api/index"));
|
|
38
|
+
const output_1 = require("../../../utils/output");
|
|
39
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
40
|
+
const index_1 = require("../../../api/deploy/index");
|
|
41
|
+
const helpers_1 = require("./helpers");
|
|
42
|
+
/** miaoda deploy error-log <deploy-id> */
|
|
43
|
+
async function handleDeployErrorLog(opts) {
|
|
44
|
+
if (!opts.deployId)
|
|
45
|
+
(0, shared_1.failArgs)("<deploy-id> 必填");
|
|
46
|
+
const appID = (0, shared_1.resolveAppId)({ appId: opts.appId });
|
|
47
|
+
(0, helpers_1.parseDeployId)(opts.deployId); // 仅校验数字形式;URL 直接用原字符串
|
|
48
|
+
const instanceID = opts.deployId;
|
|
49
|
+
const resp = await api.deploy.getErrorLog({ appID, instanceID });
|
|
50
|
+
// 信封带上 status 元信息(非 errorJobs 主体),便于 JSON 消费方读取
|
|
51
|
+
(0, output_1.emit)({
|
|
52
|
+
data: resp.errorJobs,
|
|
53
|
+
next_cursor: null,
|
|
54
|
+
has_more: false,
|
|
55
|
+
}, index_1.errorJobSchema);
|
|
56
|
+
// pretty 模式额外打一行整体状态(status 是 NodeStatus 数字)
|
|
57
|
+
if (!process.stdout.isTTY)
|
|
58
|
+
return;
|
|
59
|
+
const statusText = (0, index_1.nodeStatusText)(resp.status) ?? String(resp.status);
|
|
60
|
+
process.stdout.write(`\n— pipeline status: ${statusText}\n`);
|
|
61
|
+
}
|