@lark-apaas/miaoda-cli 0.1.0 → 0.1.1-alpha.06e1628

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 (40) hide show
  1. package/dist/api/db/api.js +248 -0
  2. package/dist/api/db/client.js +166 -0
  3. package/dist/api/db/index.js +16 -0
  4. package/dist/api/db/parsers.js +174 -0
  5. package/dist/api/db/sql-keywords.js +52 -0
  6. package/dist/api/db/types.js +10 -0
  7. package/dist/api/file/api.js +509 -0
  8. package/dist/api/file/client.js +199 -0
  9. package/dist/api/file/detect.js +56 -0
  10. package/dist/api/file/index.js +18 -0
  11. package/dist/api/file/parsers.js +72 -0
  12. package/dist/api/file/types.js +3 -0
  13. package/dist/api/index.js +5 -1
  14. package/dist/api/plugin/api.js +3 -3
  15. package/dist/cli/commands/db/index.js +209 -0
  16. package/dist/cli/commands/file/index.js +212 -0
  17. package/dist/cli/commands/index.js +4 -0
  18. package/dist/cli/commands/plugin/index.js +2 -1
  19. package/dist/cli/commands/shared.js +7 -8
  20. package/dist/cli/handlers/db/data.js +183 -0
  21. package/dist/cli/handlers/db/index.js +11 -0
  22. package/dist/cli/handlers/db/schema.js +174 -0
  23. package/dist/cli/handlers/db/sql.js +647 -0
  24. package/dist/cli/handlers/file/cp.js +221 -0
  25. package/dist/cli/handlers/file/index.js +13 -0
  26. package/dist/cli/handlers/file/ls.js +111 -0
  27. package/dist/cli/handlers/file/rm.js +264 -0
  28. package/dist/cli/handlers/file/sign.js +96 -0
  29. package/dist/cli/handlers/file/stat.js +97 -0
  30. package/dist/cli/handlers/index.js +2 -0
  31. package/dist/cli/help.js +188 -0
  32. package/dist/main.js +9 -1
  33. package/dist/utils/colors.js +98 -0
  34. package/dist/utils/error.js +14 -0
  35. package/dist/utils/fuzzy-match.js +91 -0
  36. package/dist/utils/http.js +31 -10
  37. package/dist/utils/index.js +3 -1
  38. package/dist/utils/output.js +73 -9
  39. package/dist/utils/render.js +192 -0
  40. package/package.json +4 -3
@@ -0,0 +1,96 @@
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.handleFileSign = handleFileSign;
37
+ const api = __importStar(require("../../../api/index"));
38
+ const output_1 = require("../../../utils/output");
39
+ const render_1 = require("../../../utils/render");
40
+ const error_1 = require("../../../utils/error");
41
+ const shared_1 = require("../../../cli/commands/shared");
42
+ const index_1 = require("../../../api/file/index");
43
+ const MAX_EXPIRES_SECONDS = 30 * 86400;
44
+ const DEFAULT_EXPIRES_SECONDS = 86400; // 1d(PRD 规定)
45
+ /**
46
+ * 把 <file> 解析为后端 sign 可用的 filePath。
47
+ *
48
+ * - `looksLikePath(input)`(`/path`、含 `/` 的 key、后端生成的 fileKey)→ 直接走 path,不反查
49
+ * - 其他输入按 file_name 走 FilterExpression 精确查询(多匹配 → AMBIGUOUS_FILE_NAME)
50
+ */
51
+ async function resolveFilePath(appId, input) {
52
+ if ((0, index_1.looksLikePath)(input))
53
+ return (0, index_1.toAbsolutePath)(input);
54
+ const [resolved] = await api.file.resolveInputs({
55
+ appId,
56
+ inputs: [input],
57
+ forceAs: "name",
58
+ });
59
+ if (resolved.status === "error") {
60
+ throw new error_1.AppError(resolved.error.code, resolved.error.message, {
61
+ next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
62
+ });
63
+ }
64
+ return resolved.file.path;
65
+ }
66
+ async function handleFileSign(file, opts) {
67
+ const appId = (0, shared_1.resolveAppId)(opts);
68
+ let expiresSec = DEFAULT_EXPIRES_SECONDS;
69
+ if (opts.expires) {
70
+ expiresSec = (0, render_1.parseDuration)(opts.expires);
71
+ if (expiresSec > MAX_EXPIRES_SECONDS) {
72
+ throw new error_1.AppError("FILE_SIGN_DURATION_EXCEEDED", `Expires duration '${opts.expires}' exceeds the maximum of 30d`, {
73
+ next_actions: [
74
+ `Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.`,
75
+ ],
76
+ });
77
+ }
78
+ }
79
+ const filePath = await resolveFilePath(appId, file);
80
+ const result = await api.file.signDownload({
81
+ appId,
82
+ filePath,
83
+ expiresIn: expiresSec,
84
+ });
85
+ if ((0, output_1.isJsonMode)()) {
86
+ (0, output_1.emitOk)({
87
+ file_name: result.file_name,
88
+ path: result.path,
89
+ signed_url: result.signed_url,
90
+ expires_at: result.expires_at,
91
+ });
92
+ return;
93
+ }
94
+ // 默认输出 URL 一行(pretty 与 non-TTY 一致)
95
+ (0, output_1.emit)(result.signed_url);
96
+ }
@@ -0,0 +1,97 @@
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.handleFileStat = handleFileStat;
37
+ const api = __importStar(require("../../../api/index"));
38
+ const output_1 = require("../../../utils/output");
39
+ const render_1 = require("../../../utils/render");
40
+ const error_1 = require("../../../utils/error");
41
+ const shared_1 = require("../../../cli/commands/shared");
42
+ const index_1 = require("../../../api/file/index");
43
+ /**
44
+ * 解析 `<file>` 为后端 head 可用的 filePath。
45
+ *
46
+ * - `looksLikePath(input)`(`/path`、含 `/` 的 key、后端生成的 fileKey)→ 直接 HEAD
47
+ * - 其他输入按 file_name 走 FilterExpression 精确查询(多匹配 → AMBIGUOUS_FILE_NAME)
48
+ */
49
+ async function resolveFile(appId, input) {
50
+ if ((0, index_1.looksLikePath)(input)) {
51
+ return api.file.statFile({ appId, filePath: (0, index_1.toAbsolutePath)(input) });
52
+ }
53
+ const [resolved] = await api.file.resolveInputs({
54
+ appId,
55
+ inputs: [input],
56
+ forceAs: "name",
57
+ });
58
+ if (resolved.status === "error") {
59
+ throw new error_1.AppError(resolved.error.code, resolved.error.message, {
60
+ next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
61
+ });
62
+ }
63
+ const head = await api.file.statFile({ appId, filePath: resolved.file.path });
64
+ // head 响应的 file_name 有时为空,用 list 响应补齐
65
+ if (!head.file_name)
66
+ head.file_name = resolved.file.file_name;
67
+ return head;
68
+ }
69
+ async function handleFileStat(file, opts) {
70
+ const appId = (0, shared_1.resolveAppId)(opts);
71
+ const info = await resolveFile(appId, file);
72
+ if ((0, output_1.isJsonMode)()) {
73
+ (0, output_1.emitOk)(info);
74
+ return;
75
+ }
76
+ const tty = (0, render_1.isStdoutTty)();
77
+ const pairs = [
78
+ ["file_name", info.file_name || "—"],
79
+ ["path", info.path],
80
+ [
81
+ "size",
82
+ tty
83
+ ? `${(0, render_1.formatSize)(info.size_bytes)} (${String(info.size_bytes)} bytes)`
84
+ : String(info.size_bytes),
85
+ ],
86
+ ["type", info.type || "—"],
87
+ ["uploaded_at", (0, render_1.formatTime)(info.uploaded_at, tty)],
88
+ ];
89
+ if (info.uploaded_by) {
90
+ // uploaded_by 紧跟 uploaded_at 前插入(index = "uploaded_at" 之前)
91
+ pairs.splice(pairs.length - 1, 0, ["uploaded_by", info.uploaded_by]);
92
+ }
93
+ if (info.download_url) {
94
+ pairs.push(["download_url", info.download_url]);
95
+ }
96
+ (0, output_1.emit)((0, render_1.renderKeyValue)(pairs, tty));
97
+ }
@@ -15,3 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("../../cli/handlers/plugin/index"), exports);
18
+ __exportStar(require("../../cli/handlers/file/index"), exports);
19
+ __exportStar(require("../../cli/handlers/db/index"), exports);
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MiaodaHelp = void 0;
4
+ const commander_1 = require("commander");
5
+ /**
6
+ * MiaodaHelp 重写 commander 默认的 --help 输出,使之对齐 CLI 文档规范:
7
+ *
8
+ * 1. 描述放在最前(commander 默认是 Usage 在前、描述在后)
9
+ * 2. "Options:" 重命名为 "Flags:","Global Options:" 重命名为 "Global Flags:"
10
+ * 3. Usage 段独占一行 Heading + 缩进展示 usage 行
11
+ * 4. 段落顺序:描述 → Usage → Arguments → Commands → Flags → Global Flags
12
+ * → Notes(addHelpText) → Examples(addHelpText)
13
+ * (父命令不带 Examples,由 formatHelp 末尾自动追加 "Use ... --help" 提示)
14
+ * 5. Root 命令的 local options 视作 Global Flags(root 无 command-specific flag)
15
+ * 6. 子命令隐藏 -v / --version(仅 root 暴露)和自动生成的 `help` 子命令
16
+ *
17
+ * Notes / Examples 段由各命令通过 addHelpText('after', ...) 自行追加,
18
+ * 本类不直接生成 —— 框架与文案分层。
19
+ */
20
+ class MiaodaHelp extends commander_1.Help {
21
+ // 全局默认开启:所有子命令 --help 都展示 Global Flags 段
22
+ showGlobalOptions = true;
23
+ /**
24
+ * 父级 --help 的 Commands 列表里展示子命令调用形态。spec 要求只展示
25
+ * `name <args>` 不带 `[flags]` 尾巴:
26
+ *
27
+ * - 子命令是分组(含下级 subcommand)→ 只显示 name
28
+ * - 子命令是 leaf 且配置了 usage() → "name <args>"(usage 末尾的 [flags] 去掉)
29
+ * - leaf 没配置 usage → 退回 commander 默认行为
30
+ */
31
+ subcommandTerm(cmd) {
32
+ if (cmd.commands.length > 0) {
33
+ return cmd.name();
34
+ }
35
+ const usage = cmd.usage();
36
+ if (usage) {
37
+ // 去掉末尾的 [flags] / [options],对齐 spec 的 "name <args>" 形态
38
+ const argsOnly = usage.replace(/\s*\[(?:flags|options)\]\s*$/i, "").trim();
39
+ return argsOnly ? `${cmd.name()} ${argsOnly}` : cmd.name();
40
+ }
41
+ return super.subcommandTerm(cmd);
42
+ }
43
+ /**
44
+ * 父命令的 Commands 列表里:
45
+ * - 优先用子命令的 .summary()(短摘要,对齐 spec 父级列表用的简短描述)
46
+ * - 否则取 description 首行,避免多行 description 把列表撑乱
47
+ * 叶子命令自身 --help 仍展示完整 description。
48
+ */
49
+ subcommandDescription(cmd) {
50
+ const summary = cmd.summary();
51
+ if (summary)
52
+ return summary;
53
+ const desc = super.subcommandDescription(cmd);
54
+ const idx = desc.indexOf("\n");
55
+ return idx === -1 ? desc : desc.slice(0, idx);
56
+ }
57
+ /**
58
+ * Flags 段里去掉那些已经在父命令注册过的选项(避免在 Flags 与 Global Flags
59
+ * 里重复展示,例如 --env 在 db 父级注册后,leaf 即使本地也注册一份也只在
60
+ * Global Flags 里出现一次)。
61
+ */
62
+ visibleOptions(cmd) {
63
+ const opts = super.visibleOptions(cmd);
64
+ if (!cmd.parent)
65
+ return opts;
66
+ const parentLongs = new Set();
67
+ let p = cmd.parent;
68
+ while (p) {
69
+ for (const o of p.options) {
70
+ if (o.long)
71
+ parentLongs.add(o.long);
72
+ }
73
+ p = p.parent;
74
+ }
75
+ return opts.filter((o) => !o.long || !parentLongs.has(o.long));
76
+ }
77
+ /**
78
+ * 子命令 --help 默认会从父级继承 -v, --version;spec 只在 root 列这条。
79
+ * 非 root 命令把 --version 从 Global Flags 列表里过滤掉。
80
+ */
81
+ visibleGlobalOptions(cmd) {
82
+ const opts = super.visibleGlobalOptions(cmd);
83
+ if (cmd.parent) {
84
+ return opts.filter((o) => o.long !== "--version" && o.short !== "-v");
85
+ }
86
+ return opts;
87
+ }
88
+ /**
89
+ * Root 命令默认会列 commander 自动生成的 `help [command]` 子命令;
90
+ * spec 不展示这一条,过滤掉。
91
+ */
92
+ visibleCommands(cmd) {
93
+ return super.visibleCommands(cmd).filter((c) => c.name() !== "help");
94
+ }
95
+ formatHelp(cmd, helper) {
96
+ const isRoot = cmd.parent == null;
97
+ const termWidth = helper.padWidth(cmd, helper);
98
+ const helpWidth = helper.helpWidth ?? 80;
99
+ const formatItem = (term, description) => {
100
+ if (description) {
101
+ const padding = " ".repeat(Math.max(termWidth - term.length, 0) + 2);
102
+ return `${term}${padding}${description}`;
103
+ }
104
+ return term;
105
+ };
106
+ const formatList = (lines) => lines.map((l) => " " + l).join("\n");
107
+ void helpWidth; // 保留以备后续按宽度自动 wrap,当前直接透传 description
108
+ const out = [];
109
+ // 1. 描述
110
+ const desc = helper.commandDescription(cmd);
111
+ if (desc) {
112
+ out.push(desc, "");
113
+ }
114
+ // 2. Usage:独立 heading + 缩进
115
+ out.push("Usage:", ` ${helper.commandUsage(cmd)}`, "");
116
+ // 3. Commands(仅父级命令组有,spec 要求 Commands 在 Flags 前)
117
+ // spec 不展示 Arguments 段,参数说明放在 description 文本里
118
+ const subs = helper.visibleCommands(cmd).map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
119
+ if (subs.length) {
120
+ out.push("Commands:", formatList(subs), "");
121
+ }
122
+ // 5. Flags(叶子命令专属 options)
123
+ // - Root / 父命令组:local options 都视作"会被子命令继承的 globals",渲染到 Global Flags 段
124
+ // - 叶子命令(无子命令):local options 渲染到 Flags 段(如 db data export 的 --format)
125
+ // - `-h, --help` 永远不放 Flags 段,统一放 Global Flags(spec 约定)
126
+ const isParent = subs.length > 0;
127
+ if (!isRoot && !isParent) {
128
+ const opts = helper.visibleOptions(cmd)
129
+ .filter((o) => !isHelpOption(o))
130
+ .map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
131
+ if (opts.length) {
132
+ out.push("Flags:", formatList(opts), "");
133
+ }
134
+ }
135
+ // 6. Global Flags
136
+ // - Root:local options 当 globals 渲染(root 自己就是 global 来源)
137
+ // - 父命令组:继承自祖先的 globals + 自己的 local options(也会被子命令继承)
138
+ // - 叶子命令:继承自祖先的 globals + 当前的 -h, --help
139
+ // - 末尾确保 -h, --help 一条
140
+ const localOpts = helper.visibleOptions(cmd);
141
+ let globals = [];
142
+ if (isRoot) {
143
+ globals = localOpts;
144
+ }
145
+ else if (isParent) {
146
+ const inherited = helper.visibleGlobalOptions(cmd);
147
+ const localNonHelp = localOpts.filter((o) => !isHelpOption(o));
148
+ const helpOpt = localOpts.find(isHelpOption);
149
+ globals = [...inherited, ...localNonHelp];
150
+ if (helpOpt && !globals.includes(helpOpt))
151
+ globals.push(helpOpt);
152
+ }
153
+ else if (this.showGlobalOptions) {
154
+ const inherited = helper.visibleGlobalOptions(cmd);
155
+ const helpOpt = localOpts.find(isHelpOption);
156
+ globals = [...inherited];
157
+ if (helpOpt && !globals.includes(helpOpt))
158
+ globals.push(helpOpt);
159
+ }
160
+ if (globals.length) {
161
+ const lines = globals.map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
162
+ out.push("Global Flags:", formatList(lines), "");
163
+ }
164
+ // 7. 父命令底部追加 "Use <cmd> <subcommand> --help" 提示,对齐 spec
165
+ if (subs.length > 0) {
166
+ const path = cmd.name() === "miaoda" ? "miaoda" : commandPath(cmd);
167
+ out.push(`Use "${path} <command> --help" for more information about a command.`, "");
168
+ }
169
+ // 保留末尾换行:commander 用 join('\n') 拼 addHelpText('after') 段,
170
+ // 这里多留一个 \n,让 Notes / Examples 段与上面段落之间空一行。
171
+ return out.join("\n").replace(/\n+$/, "\n");
172
+ }
173
+ }
174
+ exports.MiaodaHelp = MiaodaHelp;
175
+ /** 判断是否为 -h / --help 选项。 */
176
+ function isHelpOption(o) {
177
+ return o.long === "--help" || o.short === "-h";
178
+ }
179
+ /** 拼接命令完整路径,例如 db schema -> "miaoda db schema"。 */
180
+ function commandPath(cmd) {
181
+ const names = [];
182
+ let cur = cmd;
183
+ while (cur) {
184
+ names.unshift(cur.name());
185
+ cur = cur.parent;
186
+ }
187
+ return names.join(" ");
188
+ }
package/dist/main.js CHANGED
@@ -5,15 +5,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const commander_1 = require("commander");
7
7
  const index_1 = require("./cli/commands/index");
8
+ const help_1 = require("./cli/help");
8
9
  const config_1 = require("./utils/config");
9
10
  const log_id_1 = require("./utils/log_id");
10
11
  const output_1 = require("./utils/output");
11
12
  const package_json_1 = __importDefault(require("../package.json"));
13
+ // MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
14
+ // 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
15
+ // 子命令)统一走 MiaodaHelp 渲染,避免在每个子命令上重复 configureHelp。
16
+ commander_1.Command.prototype.createHelp = function () {
17
+ return Object.assign(new help_1.MiaodaHelp(), this.configureHelp());
18
+ };
12
19
  const program = new commander_1.Command();
13
20
  const { version } = package_json_1.default;
14
21
  program
15
22
  .name("miaoda")
16
- .description("妙搭平台命令行工具")
23
+ .description("妙搭平台 CLI,提供数据服务、文件存储、插件管理等命令行操作。")
24
+ .usage("<command> [flags]")
17
25
  .version(version, "-v, --version", "显示版本号")
18
26
  .option("--json [fields]", "JSON 输出,可选字段级选择")
19
27
  .option("--output <format>", "输出格式(pretty|json)", "pretty")
@@ -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
+ };
@@ -5,12 +5,22 @@ class AppError extends Error {
5
5
  code;
6
6
  retryable;
7
7
  next_actions;
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;
8
17
  constructor(code, message, opts) {
9
18
  super(message);
10
19
  this.name = "AppError";
11
20
  this.code = code;
12
21
  this.retryable = opts?.retryable ?? false;
13
22
  this.next_actions = opts?.next_actions ?? [];
23
+ this.statement_index = opts?.statement_index;
14
24
  }
15
25
  toJSON() {
16
26
  return {
@@ -18,6 +28,10 @@ class AppError extends Error {
18
28
  message: this.message,
19
29
  retryable: this.retryable,
20
30
  next_actions: this.next_actions,
31
+ statement_index: this.statement_index,
32
+ total_statements: this.total_statements,
33
+ completed: this.completed,
34
+ rolled_back: this.rolled_back,
21
35
  };
22
36
  }
23
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
+ }