@lark-apaas/miaoda-cli 0.1.1 → 0.1.2-alpha.2e37617

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.
Files changed (37) hide show
  1. package/dist/api/db/api.js +333 -12
  2. package/dist/api/db/client.js +76 -29
  3. package/dist/api/db/index.js +9 -1
  4. package/dist/api/db/parsers.js +33 -20
  5. package/dist/api/db/sql-keywords.js +123 -0
  6. package/dist/api/file/api.js +93 -24
  7. package/dist/api/file/client.js +1 -5
  8. package/dist/api/file/index.js +2 -1
  9. package/dist/api/file/parsers.js +1 -5
  10. package/dist/api/plugin/api.js +8 -3
  11. package/dist/cli/commands/db/index.js +138 -0
  12. package/dist/cli/commands/file/index.js +7 -0
  13. package/dist/cli/commands/plugin/index.js +18 -6
  14. package/dist/cli/commands/shared.js +1 -3
  15. package/dist/cli/handlers/db/audit.js +285 -0
  16. package/dist/cli/handlers/db/changelog.js +117 -0
  17. package/dist/cli/handlers/db/data.js +23 -3
  18. package/dist/cli/handlers/db/index.js +17 -1
  19. package/dist/cli/handlers/db/migration.js +147 -0
  20. package/dist/cli/handlers/db/quota.js +68 -0
  21. package/dist/cli/handlers/db/recovery.js +188 -0
  22. package/dist/cli/handlers/db/schema.js +22 -8
  23. package/dist/cli/handlers/db/sql.js +304 -16
  24. package/dist/cli/handlers/file/cp.js +39 -17
  25. package/dist/cli/handlers/file/index.js +3 -1
  26. package/dist/cli/handlers/file/ls.js +1 -3
  27. package/dist/cli/handlers/file/quota.js +66 -0
  28. package/dist/cli/handlers/file/rm.js +4 -3
  29. package/dist/cli/handlers/plugin/plugin-local.js +23 -9
  30. package/dist/cli/handlers/plugin/plugin.js +21 -7
  31. package/dist/cli/help.js +5 -2
  32. package/dist/utils/colors.js +98 -0
  33. package/dist/utils/error.js +11 -0
  34. package/dist/utils/fuzzy-match.js +91 -0
  35. package/dist/utils/output.js +81 -5
  36. package/dist/utils/render.js +61 -41
  37. package/package.json +10 -2
@@ -82,7 +82,9 @@ function parsePluginName(input) {
82
82
  function readPackageJson() {
83
83
  const pkgPath = getPackageJsonPath();
84
84
  if (!node_fs_1.default.existsSync(pkgPath)) {
85
- throw new error_1.AppError("PKG_JSON_NOT_FOUND", "package.json not found in current directory", { next_actions: ["在应用项目根目录运行"] });
85
+ throw new error_1.AppError("PKG_JSON_NOT_FOUND", "package.json not found in current directory", {
86
+ next_actions: ["在应用项目根目录运行"],
87
+ });
86
88
  }
87
89
  const content = node_fs_1.default.readFileSync(pkgPath, "utf-8");
88
90
  return JSON.parse(content);
@@ -189,9 +191,14 @@ function installMissingDeps(deps) {
189
191
  if (deps.length === 0)
190
192
  return;
191
193
  (0, logger_1.log)("plugin", `Installing missing dependencies: ${deps.join(", ")}`);
192
- const result = (0, node_child_process_1.spawnSync)("npm", ["install", ...deps, "--no-save", "--no-package-lock"], { cwd: getProjectRoot(), stdio: "inherit" });
194
+ const result = (0, node_child_process_1.spawnSync)("npm", ["install", ...deps, "--no-save", "--no-package-lock"], {
195
+ cwd: getProjectRoot(),
196
+ stdio: "inherit",
197
+ });
193
198
  if (result.error) {
194
- throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, { next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"] });
199
+ throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
200
+ next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"],
201
+ });
195
202
  }
196
203
  if (result.status !== 0) {
197
204
  throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed with exit code ${String(result.status)}`, { next_actions: ["检查上方 npm 输出日志定位具体错误"] });
@@ -200,7 +207,9 @@ function installMissingDeps(deps) {
200
207
  function npmInstall(tgzPath) {
201
208
  const result = (0, node_child_process_1.spawnSync)("npm", ["install", tgzPath, "--no-save", "--no-package-lock", "--ignore-scripts"], { cwd: getProjectRoot(), stdio: "inherit" });
202
209
  if (result.error) {
203
- throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, { next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"] });
210
+ throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed: ${result.error.message}`, {
211
+ next_actions: ["确认本机已安装 npm,可 --verbose 查看执行详情"],
212
+ });
204
213
  }
205
214
  if (result.status !== 0) {
206
215
  throw new error_1.AppError("INTERNAL_NPM_FAILED", `npm install failed with exit code ${String(result.status)}`, { next_actions: ["检查上方 npm 输出日志定位具体错误"] });
@@ -232,7 +241,9 @@ function listCapabilityIds() {
232
241
  function readCapability(id) {
233
242
  const filePath = node_path_1.default.join(getCapabilitiesDir(), `${id}.json`);
234
243
  if (!node_fs_1.default.existsSync(filePath)) {
235
- throw new error_1.AppError("CAPABILITY_NOT_FOUND", `Capability not found: ${id}`, { next_actions: ["运行 miaoda plugin list 查看所有可用 capability id"] });
244
+ throw new error_1.AppError("CAPABILITY_NOT_FOUND", `Capability not found: ${id}`, {
245
+ next_actions: ["运行 miaoda plugin list 查看所有可用 capability id"],
246
+ });
236
247
  }
237
248
  try {
238
249
  const content = node_fs_1.default.readFileSync(filePath, "utf-8");
@@ -240,7 +251,9 @@ function readCapability(id) {
240
251
  }
241
252
  catch (error) {
242
253
  if (error instanceof SyntaxError) {
243
- throw new error_1.AppError("INVALID_JSON", `Invalid JSON in capability file: ${id}.json`, { next_actions: [`检查 server/capabilities/${id}.json 的 JSON 语法`] });
254
+ throw new error_1.AppError("INVALID_JSON", `Invalid JSON in capability file: ${id}.json`, {
255
+ next_actions: [`检查 server/capabilities/${id}.json 的 JSON 语法`],
256
+ });
244
257
  }
245
258
  throw error;
246
259
  }
@@ -303,7 +316,9 @@ async function loadPlugin(pluginKey) {
303
316
  }
304
317
  catch (error) {
305
318
  if (error.code === "MODULE_NOT_FOUND") {
306
- throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not installed: ${pluginKey}`, { next_actions: [`运行 miaoda plugin install ${pluginKey}`] });
319
+ throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not installed: ${pluginKey}`, {
320
+ next_actions: [`运行 miaoda plugin install ${pluginKey}`],
321
+ });
307
322
  }
308
323
  throw new error_1.AppError("INTERNAL_PLUGIN_LOAD_FAILED", `Failed to load plugin ${pluginKey}: ${error instanceof Error ? error.message : String(error)}`);
309
324
  }
@@ -314,8 +329,7 @@ async function hydrateCapability(capability) {
314
329
  if (manifest.actions.length === 0) {
315
330
  throw new error_1.AppError("INTERNAL_PLUGIN_LOAD_FAILED", `Plugin ${capability.pluginKey} has no actions defined`);
316
331
  }
317
- const hasDynamic = manifest.actions.some((action) => isDynamicSchema(action.inputSchema) ||
318
- isDynamicSchema(action.outputSchema));
332
+ const hasDynamic = manifest.actions.some((action) => isDynamicSchema(action.inputSchema) || isDynamicSchema(action.outputSchema));
319
333
  let pluginInstance = null;
320
334
  if (hasDynamic) {
321
335
  const plugin = await loadPlugin(capability.pluginKey);
@@ -44,7 +44,9 @@ const output_1 = require("../../../utils/output");
44
44
  const error_1 = require("../../../utils/error");
45
45
  const logger_1 = require("../../../utils/logger");
46
46
  const plugin_local_1 = require("./plugin-local");
47
- const log = (msg) => { (0, logger_1.log)("plugin", msg); };
47
+ const log = (msg) => {
48
+ (0, logger_1.log)("plugin", msg);
49
+ };
48
50
  // ── Install ──
49
51
  function syncActionPluginsRecord(name, version) {
50
52
  const plugins = (0, plugin_local_1.readActionPlugins)();
@@ -63,7 +65,9 @@ async function installOne(nameWithVersion) {
63
65
  if (actualVersion === requestedVersion) {
64
66
  log(`${name}@${requestedVersion} already installed`);
65
67
  syncActionPluginsRecord(name, actualVersion);
66
- api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => { });
68
+ api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
69
+ /* fire-and-forget */
70
+ });
67
71
  return { name, version: actualVersion, success: true, skipped: true };
68
72
  }
69
73
  }
@@ -74,7 +78,9 @@ async function installOne(nameWithVersion) {
74
78
  if (actualVersion === targetVersion) {
75
79
  log(`${name} already up to date (${actualVersion})`);
76
80
  syncActionPluginsRecord(name, actualVersion);
77
- api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => { });
81
+ api.plugin.reportCreateInstanceEvent(name, actualVersion).catch(() => {
82
+ /* fire-and-forget */
83
+ });
78
84
  return { name, version: actualVersion, success: true, skipped: true };
79
85
  }
80
86
  log(`Found newer version: ${targetVersion} (installed: ${actualVersion ?? "none"})`);
@@ -106,8 +112,12 @@ async function installOne(nameWithVersion) {
106
112
  (0, plugin_local_1.writeActionPlugins)(plugins);
107
113
  const source = fromCache ? "from cache" : "downloaded";
108
114
  log(`Installed ${name}@${installedVersion} (${source})`);
109
- api.plugin.reportInstallEvent(name, installedVersion).catch(() => { });
110
- api.plugin.reportCreateInstanceEvent(name, installedVersion).catch(() => { });
115
+ api.plugin.reportInstallEvent(name, installedVersion).catch(() => {
116
+ /* fire-and-forget */
117
+ });
118
+ api.plugin.reportCreateInstanceEvent(name, installedVersion).catch(() => {
119
+ /* fire-and-forget */
120
+ });
111
121
  return { name, version: installedVersion, success: true };
112
122
  }
113
123
  catch (error) {
@@ -193,7 +203,9 @@ async function handlePluginUpdate(opts) {
193
203
  function handlePluginRemove(opts) {
194
204
  const { name } = (0, plugin_local_1.parsePluginName)(opts.name);
195
205
  if (!(0, plugin_local_1.isPluginInstalled)(name)) {
196
- throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin ${name} is not installed`, { next_actions: ["运行 miaoda plugin list-packages 查看已安装插件"] });
206
+ throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin ${name} is not installed`, {
207
+ next_actions: ["运行 miaoda plugin list-packages 查看已安装插件"],
208
+ });
197
209
  }
198
210
  (0, plugin_local_1.removePluginDirectory)(name);
199
211
  const plugins = (0, plugin_local_1.readActionPlugins)();
@@ -263,7 +275,9 @@ async function handlePluginInit() {
263
275
  // ── List (capability configs) ──
264
276
  async function handlePluginList(opts) {
265
277
  if (!(0, plugin_local_1.capabilitiesDirExists)()) {
266
- throw new error_1.AppError("CAPABILITIES_DIR_NOT_FOUND", "server/capabilities directory not found", { next_actions: ["当前目录必须是含 server/capabilities/ 的应用项目"] });
278
+ throw new error_1.AppError("CAPABILITIES_DIR_NOT_FOUND", "server/capabilities directory not found", {
279
+ next_actions: ["当前目录必须是含 server/capabilities/ 的应用项目"],
280
+ });
267
281
  }
268
282
  if (opts.id) {
269
283
  const capability = (0, plugin_local_1.readCapability)(opts.id);
package/dist/cli/help.js CHANGED
@@ -115,7 +115,9 @@ class MiaodaHelp extends commander_1.Help {
115
115
  out.push("Usage:", ` ${helper.commandUsage(cmd)}`, "");
116
116
  // 3. Commands(仅父级命令组有,spec 要求 Commands 在 Flags 前)
117
117
  // spec 不展示 Arguments 段,参数说明放在 description 文本里
118
- const subs = helper.visibleCommands(cmd).map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
118
+ const subs = helper
119
+ .visibleCommands(cmd)
120
+ .map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
119
121
  if (subs.length) {
120
122
  out.push("Commands:", formatList(subs), "");
121
123
  }
@@ -125,7 +127,8 @@ class MiaodaHelp extends commander_1.Help {
125
127
  // - `-h, --help` 永远不放 Flags 段,统一放 Global Flags(spec 约定)
126
128
  const isParent = subs.length > 0;
127
129
  if (!isRoot && !isParent) {
128
- const opts = helper.visibleOptions(cmd)
130
+ const opts = helper
131
+ .visibleOptions(cmd)
129
132
  .filter((o) => !isHelpOption(o))
130
133
  .map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
131
134
  if (opts.length) {
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * 终端彩色高亮的语义层封装。
4
+ *
5
+ * 三层结构(自上而下:稳定→灵活):
6
+ *
7
+ * 1. **ColorAdapter**(公共契约):6 个语义方法 string → string,业务代码
8
+ * 只接触这层,对底层库 0 感知。
9
+ * 2. **ColorFactory**(适配器签名):`(useColor: boolean) => ColorAdapter`,
10
+ * 把"是否染色"参数化,便于按需构造。一个三方库对应一个 factory 实现。
11
+ * 3. **当前底层库实现**:picocolors([picocolors npm](https://www.npmjs.com/package/picocolors),~3KB)。
12
+ * 切换到 chalk / kleur 等只需新增一个 ColorFactory 实现,并改最下方的
13
+ * `factory =` 一行赋值,业务代码与 ColorAdapter contract 0 改动。
14
+ *
15
+ * # 色谱(对齐 spec)
16
+ *
17
+ * - header 表头 / 命令名 / key 标签:bold + cyan
18
+ * - highlight 强调值(表名等):cyan
19
+ * - success ✓ Uploaded / ✓ Deleted:green
20
+ * - fail ✗ / Error::red
21
+ * - warn ⚠ Approaching quota:yellow
22
+ * - muted NULL / 辅助 hint / 次要信息:dim + gray
23
+ *
24
+ * # 染色判定(NO_COLOR / FORCE_COLOR / TTY,[no-color.org](https://no-color.org/) 业界标准)
25
+ *
26
+ * - `NO_COLOR=1` 强制关颜色(任何非空值生效)
27
+ * - `FORCE_COLOR=1` 强制开颜色(管道 / CI 场景)
28
+ * - 否则按 `process.stdout.isTTY` 判定
29
+ *
30
+ * 每次调用 `c.xxx(s)` 时按当前状态实时构造 adapter——避免模块加载时缓存的
31
+ * `process.stdout.isTTY` 与测试 / pipe 切换脱节。
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.c = void 0;
35
+ exports.shouldColorize = shouldColorize;
36
+ const picocolors_1 = require("picocolors");
37
+ // ──────────────────────────────────────────────────────────────────────
38
+ // 2. 库无关的染色判定:哪个 factory 都能复用
39
+ // ──────────────────────────────────────────────────────────────────────
40
+ /**
41
+ * 当前是否应当染色。读取的是**调用时**的状态,而非模块加载时缓存——
42
+ * 测试经常 `Object.defineProperty(process.stdout, "isTTY", ...)` 实时切换,
43
+ * 这里直接每次问 process。
44
+ *
45
+ * 优先级:FORCE_COLOR > NO_COLOR > TTY。
46
+ */
47
+ function shouldColorize() {
48
+ const env = process.env;
49
+ const noColor = env.NO_COLOR != null && env.NO_COLOR !== "";
50
+ const forceColor = env.FORCE_COLOR != null && env.FORCE_COLOR !== "";
51
+ const isTTY = process.stdout.isTTY === true;
52
+ return forceColor || (!noColor && isTTY);
53
+ }
54
+ /** picocolors 实现:当前生产用 factory。~3KB,业界 CLI 标准之一。 */
55
+ const picocolorsFactory = (useColor) => {
56
+ const x = (0, picocolors_1.createColors)(useColor);
57
+ return {
58
+ header: (s) => x.bold(x.cyan(s)),
59
+ highlight: (s) => x.cyan(s),
60
+ success: (s) => x.green(s),
61
+ fail: (s) => x.red(s),
62
+ warn: (s) => x.yellow(s),
63
+ muted: (s) => x.dim(x.gray(s)),
64
+ };
65
+ };
66
+ // ──────────────────────────────────────────────────────────────────────
67
+ // 4. 唯一切换点:换库改这一行(其它都不用动)
68
+ // ──────────────────────────────────────────────────────────────────────
69
+ //
70
+ // 切换示例:
71
+ //
72
+ // import chalk, { Chalk } from "chalk";
73
+ // const chalkFactory: ColorFactory = (useColor) => {
74
+ // const k = new Chalk({ level: useColor ? 1 : 0 });
75
+ // return {
76
+ // header: (s) => k.bold.cyan(s),
77
+ // highlight: (s) => k.cyan(s),
78
+ // success: (s) => k.green(s),
79
+ // fail: (s) => k.red(s),
80
+ // warn: (s) => k.yellow(s),
81
+ // muted: (s) => k.dim.gray(s),
82
+ // };
83
+ // };
84
+ // const factory: ColorFactory = chalkFactory; // ← 改这里
85
+ //
86
+ const factory = picocolorsFactory;
87
+ // ──────────────────────────────────────────────────────────────────────
88
+ // 5. 公共出口:业务代码 import { c } from "../utils/colors"
89
+ // ──────────────────────────────────────────────────────────────────────
90
+ /** 语义染色器:业务代码唯一接触的对象。底层库切换 0 感知。 */
91
+ exports.c = {
92
+ header: (s) => factory(shouldColorize()).header(s),
93
+ highlight: (s) => factory(shouldColorize()).highlight(s),
94
+ success: (s) => factory(shouldColorize()).success(s),
95
+ fail: (s) => factory(shouldColorize()).fail(s),
96
+ warn: (s) => factory(shouldColorize()).warn(s),
97
+ muted: (s) => factory(shouldColorize()).muted(s),
98
+ };
@@ -6,6 +6,14 @@ class AppError extends Error {
6
6
  retryable;
7
7
  next_actions;
8
8
  statement_index;
9
+ total_statements;
10
+ /**
11
+ * 多语句失败时由 api.db.execSql 透传服务端 results(已成功的 statement 原始结构)。
12
+ * 由 db sql handler 转成 PRD 友好的 `completed` 数组 + 推断 `rolled_back` 后挂回到本对象。
13
+ */
14
+ partial_results;
15
+ completed;
16
+ rolled_back;
9
17
  constructor(code, message, opts) {
10
18
  super(message);
11
19
  this.name = "AppError";
@@ -21,6 +29,9 @@ class AppError extends Error {
21
29
  retryable: this.retryable,
22
30
  next_actions: this.next_actions,
23
31
  statement_index: this.statement_index,
32
+ total_statements: this.total_statements,
33
+ completed: this.completed,
34
+ rolled_back: this.rolled_back,
24
35
  };
25
36
  }
26
37
  }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * 模糊匹配:基于 Levenshtein 编辑距离的"did-you-mean"建议器。
4
+ *
5
+ * 用于 CLI 错误路径的拼写纠错提示——当用户输入的 token(关键字 / 表名 / 列名)
6
+ * 与某个有效候选词"接近"时,给出 "Did you mean X?" 类的 hint。
7
+ *
8
+ * # 设计取舍
9
+ *
10
+ * - **算法**:Damerau-Levenshtein DP(在标准 Levenshtein 基础上把"相邻字符
11
+ * 交换"算 1 步编辑——SQL 用户最常见的拼写错误就是 FORM/FROM、SELCT/SELECT
12
+ * 这类 transposition;纯 Levenshtein 把它们算 2 步会过严,触发不到 hint)。
13
+ * 约 30 行二维 DP 实现,不引第三方库;业界 CLI 拼写纠错事实标准。
14
+ * - **大小写不敏感**:CLI 用户混用 SELECT / select 是常态,统一 lowercase 比较
15
+ * - **阈值按候选词长度自适应**:避免短词过度匹配("id" 不应该建议成 "in")
16
+ * len ≤ 4:max 1 编辑(短词严)
17
+ * len 5-8:max 2 编辑
18
+ * len ≥ 9:max 3 编辑(长词宽)
19
+ * - **同分多候选**:返第一个;调用方负责候选词列表的稳定排序
20
+ * - **找不到不强凑**:阈值外返 null,不"硬贴"无关建议
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.levenshtein = levenshtein;
24
+ exports.suggest = suggest;
25
+ /**
26
+ * 计算两个字符串的 Damerau-Levenshtein 编辑距离(不区分大小写)。
27
+ *
28
+ * 在标准 Levenshtein(增 / 删 / 替换)基础上加"相邻字符交换"作为单步编辑。
29
+ * 二维 DP 实现(transposition 需要回看 dp[i-2][j-2],所以不再用单行优化)。
30
+ * 候选词通常 < 30 字符,二维 DP 的常数空间开销可忽略。
31
+ */
32
+ function levenshtein(a, b) {
33
+ const aa = a.toLowerCase();
34
+ const bb = b.toLowerCase();
35
+ const m = aa.length;
36
+ const n = bb.length;
37
+ if (m === 0)
38
+ return n;
39
+ if (n === 0)
40
+ return m;
41
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
42
+ for (let i = 0; i <= m; i++)
43
+ dp[i][0] = i;
44
+ for (let j = 0; j <= n; j++)
45
+ dp[0][j] = j;
46
+ for (let i = 1; i <= m; i++) {
47
+ for (let j = 1; j <= n; j++) {
48
+ const cost = aa[i - 1] === bb[j - 1] ? 0 : 1;
49
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, // 删除
50
+ dp[i][j - 1] + 1, // 插入
51
+ dp[i - 1][j - 1] + cost);
52
+ // transposition: 当前字符 = 对方前一字符 && 我方前一字符 = 对方当前字符
53
+ // 例:aa="form", bb="from",i=2,j=2 时 aa[1]=o=bb[0]=f? 不命中;i=3,j=3
54
+ // 时 aa[2]=r=bb[1]=r? bb[2]=o, aa[1]=o → 命中 → dp[3][3] = dp[1][1] + 1 = 1
55
+ if (i > 1 && j > 1 && aa[i - 1] === bb[j - 2] && aa[i - 2] === bb[j - 1]) {
56
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
57
+ }
58
+ }
59
+ }
60
+ return dp[m][n];
61
+ }
62
+ /** 默认阈值:候选词越短匹配越严,越长越宽。避免 "id" 错配 "in" 这种 false positive。 */
63
+ function defaultThreshold(len) {
64
+ if (len <= 4)
65
+ return 1;
66
+ if (len <= 8)
67
+ return 2;
68
+ return 3;
69
+ }
70
+ /**
71
+ * 在候选词列表里找与 input 最相近的一个。
72
+ *
73
+ * 距离 ≤ 阈值时按"距离最小"返回;多个候选词相同距离时返"列表中最先出现"的;
74
+ * 阈值外没有命中返 null(调用方按 null 静默不加 hint,不要强凑)。
75
+ */
76
+ function suggest(input, candidates, opts) {
77
+ if (!input || candidates.length === 0)
78
+ return null;
79
+ const threshold = opts?.maxDistance ?? defaultThreshold;
80
+ let best = null;
81
+ let bestDist = Number.POSITIVE_INFINITY;
82
+ for (const cand of candidates) {
83
+ const d = levenshtein(input, cand);
84
+ const limit = threshold(cand.length);
85
+ if (d <= limit && d < bestDist) {
86
+ best = cand;
87
+ bestDist = d;
88
+ }
89
+ }
90
+ return best;
91
+ }
@@ -5,8 +5,30 @@ exports.emit = emit;
5
5
  exports.emitError = emitError;
6
6
  exports.emitOk = emitOk;
7
7
  exports.emitPaged = emitPaged;
8
+ exports.snakeCaseKeys = snakeCaseKeys;
8
9
  const config_1 = require("./config");
9
10
  const error_1 = require("./error");
11
+ const colors_1 = require("./colors");
12
+ /**
13
+ * 服务端错误码 → CLI 兜底 hint 文案。
14
+ *
15
+ * 服务端错误协议只有 `{code, msg}` 两个字段(dataloom InnerExecuteSQL 等 IDL
16
+ * 没有 hint / next_actions 通道),所以服务端给的错误本身永远没有 hint。
17
+ * 这里是 CLI 展示层为常见错误码补一份 spec 一致的 actionable 引导,按错误码
18
+ * 落到 next_actions。
19
+ *
20
+ * 仅在 next_actions 已经为空时介入——保留 handler / enrichSqlError 自己塞过的
21
+ * 具体 hint(如 did-you-mean、shared.resolveAppId 等),它们优先级更高。
22
+ */
23
+ const SERVER_ERROR_HINTS = {
24
+ // SELECT 结果集超过 1000 行硬拦:spec 多行引导。
25
+ // key 用 BIZ_ERR_MAP 映射后的语义 code(不是原始服务端 k_dl_xxx),保持
26
+ // 与 help / 文档里宣传的 RESULT_SET_TOO_LARGE 一致。
27
+ RESULT_SET_TOO_LARGE: [
28
+ "Add `LIMIT <n>` to your SQL to narrow the result.",
29
+ "For counts, use `SELECT count(*) FROM ...`; for aggregation, use GROUP BY with filters.",
30
+ ],
31
+ };
10
32
  function isJsonMode() {
11
33
  const cfg = (0, config_1.getConfig)();
12
34
  return cfg.output === "json" || Boolean(cfg.json);
@@ -36,23 +58,56 @@ function emit(data) {
36
58
  */
37
59
  function emitError(err) {
38
60
  const info = toErrorInfo(err);
61
+ // 没人给过 next_actions 才走错误码兜底;handler 已经塞过具体 hint 时不覆盖
62
+ const hints = info.next_actions && info.next_actions.length > 0
63
+ ? info.next_actions
64
+ : (SERVER_ERROR_HINTS[info.code] ?? []);
39
65
  if (isJsonMode()) {
40
66
  const errObj = {
41
67
  code: info.code,
42
68
  message: info.message,
43
69
  };
44
- if (info.next_actions && info.next_actions.length > 0) {
45
- errObj.hint = info.next_actions.join(" ");
70
+ if (hints.length > 0) {
71
+ // JSON 输出压平成单行,更便于机器消费(脚本 / agent 拼字符串)
72
+ errObj.hint = hints.join(" ");
46
73
  }
47
74
  if (typeof info.statement_index === "number") {
48
75
  errObj.statement_index = info.statement_index;
49
76
  }
77
+ // PRD 多语句失败 envelope 额外字段:completed / rolled_back
78
+ if (Array.isArray(info.completed)) {
79
+ errObj.completed = info.completed;
80
+ }
81
+ if (typeof info.rolled_back === "boolean") {
82
+ errObj.rolled_back = info.rolled_back;
83
+ }
50
84
  process.stderr.write(JSON.stringify({ error: errObj }) + "\n");
51
85
  }
52
86
  else {
53
- process.stderr.write(`Error: ${info.message}\n`);
54
- if (info.next_actions && info.next_actions.length > 0) {
55
- process.stderr.write(` hint: ${info.next_actions.join(" ")}\n`);
87
+ // stderr 染色:picocolors 默认按 stdout.isTTY 判断;stderr 通常也是 tty,
88
+ // 这里复用 stdout 探测保持简单(stderr only-pipe 的极端场景留作后续优化)
89
+ // 多语句失败时 Error 行末尾追加 "(at statement K of N)",与 PRD spec 对齐
90
+ let errorLine = `${colors_1.c.fail("Error:")} ${info.message}`;
91
+ if (typeof info.statement_index === "number") {
92
+ const k = info.statement_index + 1;
93
+ const n = info.total_statements;
94
+ errorLine +=
95
+ typeof n === "number" && n > 0
96
+ ? ` (at statement ${String(k)} of ${String(n)})`
97
+ : ` (at statement ${String(k)})`;
98
+ }
99
+ process.stderr.write(errorLine + "\n");
100
+ // 多行 hint:第一行带 "hint:" 标签,后续行用 8 空格缩进对齐 " hint: " 之后的列。
101
+ // 对应 spec 期望的格式:
102
+ // Error: ...
103
+ // hint: 第一条建议
104
+ // 第二条建议
105
+ if (hints.length > 0) {
106
+ const [first, ...rest] = hints;
107
+ process.stderr.write(` ${colors_1.c.muted("hint:")} ${first}\n`);
108
+ for (const line of rest) {
109
+ process.stderr.write(` ${line}\n`);
110
+ }
56
111
  }
57
112
  }
58
113
  }
@@ -64,6 +119,27 @@ function emitOk(data) {
64
119
  function emitPaged(items, nextCursor, hasMore) {
65
120
  emit({ data: items, next_cursor: nextCursor, has_more: hasMore });
66
121
  }
122
+ /**
123
+ * 把对象 / 数组里所有字符串 key 从 camelCase 转成 snake_case,递归处理嵌套对象。
124
+ * 后端 IDL 返回的 JSON 字段是 camelCase(`storageUsedBytes`),CLI 对外(PRD)
125
+ * 统一 snake_case(`storage_used_bytes`);emit JSON 前过一遍这个函数。
126
+ */
127
+ function snakeCaseKeys(input) {
128
+ if (Array.isArray(input)) {
129
+ return input.map((item) => snakeCaseKeys(item));
130
+ }
131
+ if (input !== null && typeof input === "object") {
132
+ const out = {};
133
+ for (const [k, v] of Object.entries(input)) {
134
+ out[camelToSnake(k)] = snakeCaseKeys(v);
135
+ }
136
+ return out;
137
+ }
138
+ return input;
139
+ }
140
+ function camelToSnake(s) {
141
+ return s.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
142
+ }
67
143
  function toErrorInfo(err) {
68
144
  if (err instanceof error_1.AppError)
69
145
  return err.toJSON();