@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,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerFileCommands = registerFileCommands;
4
+ const index_1 = require("../../../cli/handlers/file/index");
5
+ const error_1 = require("../../../utils/error");
6
+ /**
7
+ * commander option 校验器:把 --limit <n> 解析成正整数(≥1)。
8
+ * 默认值(如 "50")会先经过这里被规范化成 number。
9
+ * 非整数 / 负数 / 0 抛 AppError("ARGS_INVALID"),由 main.ts 的全局 catch
10
+ * 走 emitError,同时 process.exitCode 由 commander 自然为 1。
11
+ */
12
+ function parsePositiveInt(raw) {
13
+ const n = Number(raw);
14
+ if (!Number.isInteger(n) || n < 1) {
15
+ throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
16
+ }
17
+ return n;
18
+ }
19
+ function registerFileCommands(program) {
20
+ const fileCmd = program
21
+ .command("file")
22
+ .description("应用文件存储(TOS)的命令行操作集合。操作对象是 UGC 资源(用户上传的文件、应用运行时\n" +
23
+ "生成的报表 / 导出文件等),不涉及代码仓库里的本地文件。")
24
+ .usage("<command> [flags]");
25
+ fileCmd.action(() => {
26
+ fileCmd.outputHelp();
27
+ });
28
+ fileCmd
29
+ .command("ls")
30
+ .summary("列出 / 筛选文件")
31
+ .description("列出当前应用存储里的文件,支持按文件名、大小、上传时间筛选,以及游标分页。\n" +
32
+ "默认 pretty 输出省略 download_url(列宽限制),需获取请用 --json。")
33
+ .usage("[query] [flags]")
34
+ .argument("[query]", "筛选值:以 / 开头视为路径精确匹配,否则按文件名精确匹配")
35
+ .option("--path <path>", "按路径精确匹配")
36
+ .option("--name <name>", "按文件名精确匹配")
37
+ .option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
38
+ .option("--size-gt <size>", "文件大小下限(支持 B / KB / MB / GB)")
39
+ .option("--size-lt <size>", "文件大小上限(支持 B / KB / MB / GB)")
40
+ .option("--uploaded-since <time>", "上传时间下限(晚于该时间),支持:相对时间 1h / 2d / 1w;日期 YYYY-MM-DD;ISO 8601 如 2026-04-01T10:00:00Z")
41
+ .option("--uploaded-until <time>", "上传时间上限(早于该时间),支持:相对时间 1h / 2d / 1w;日期 YYYY-MM-DD;ISO 8601 如 2026-04-01T10:00:00Z")
42
+ .option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
43
+ .option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
44
+ .option("--all", "自动翻页返回全部结果")
45
+ .action(async (query, opts) => {
46
+ await (0, index_1.handleFileLs)({ ...opts, query });
47
+ })
48
+ .addHelpText("after", `
49
+ Examples:
50
+ $ miaoda file ls
51
+ file_name path size type uploaded_at
52
+ logo.png /images/brand/1858537546760216.png 24 KB image/png 3h ago
53
+ hero.jpg /images/1858537546760217.jpg 128 KB image/jpeg 2d ago
54
+ report.pdf /docs/1858537546760218.pdf 2.1 MB application/pdf 2026-04-10
55
+
56
+ $ miaoda file ls --name logo.png
57
+ file_name path size type uploaded_at
58
+ logo.png /images/brand/1858537546760216.png 24 KB image/png 3h ago
59
+
60
+ $ miaoda file ls --uploaded-since 7d --size-gt 1MB
61
+ ...
62
+ `);
63
+ fileCmd
64
+ .command("stat")
65
+ .summary("查看单文件元数据")
66
+ .description("查看单文件完整元数据,含 download_url(应用内消费)。\n" +
67
+ "需要公网可访问的临时链接请用 `file sign` 生成 signed_url。")
68
+ .usage("<file> [flags]")
69
+ .argument("<file>", "文件的路径或文件名(自动识别)")
70
+ .action(async (file, opts) => {
71
+ await (0, index_1.handleFileStat)(file, opts);
72
+ })
73
+ .addHelpText("after", `
74
+ Notes:
75
+ - <file> 可传 path(推荐,全局唯一)或 file_name。
76
+ - file_name 重名时报 AMBIGUOUS_FILE_NAME,需先 \`ls --name\` 拿到 path 再调用。
77
+
78
+ Examples:
79
+ $ miaoda file stat /images/brand/1858537546760216.png
80
+ file_name: logo.png
81
+ path: /images/brand/1858537546760216.png
82
+ size: 24 KB (24580 bytes)
83
+ type: image/png
84
+ uploaded_by: alice
85
+ uploaded_at: 2026-04-15 10:30:00
86
+ download_url: /spark/app/.../1858537546760216.png
87
+
88
+ # 报错:file_name 多匹配
89
+ $ miaoda file stat logo.png
90
+ Error: Multiple files match name 'logo.png' (2 found)
91
+ hint: Use path instead. Run \`miaoda file ls --name logo.png\` to see candidates.
92
+ `);
93
+ fileCmd
94
+ .command("cp")
95
+ .summary("上传或下载文件(方向由路径前缀自动判断)")
96
+ .description("上传或下载文件,方向由 src/dst 路径前缀自动判断:\n" +
97
+ " / 开头 → 远程 TOS 路径\n" +
98
+ " ./、~/、裸文件名 → 本地路径")
99
+ .usage("<src> <dst> [flags]")
100
+ .argument("<src>", "源:本地文件路径或远程文件路径 / 文件名")
101
+ .argument("<dst>", "目标:本地路径或远程路径")
102
+ .option("--rename <name>", "上传后在远端使用的新文件名")
103
+ .action(async (src, dst, opts) => {
104
+ await (0, index_1.handleFileCp)(src, dst, opts);
105
+ })
106
+ .addHelpText("after", `
107
+ Notes:
108
+ - 单文件上限 100 MB,超过请拆分或用 web console 上传。
109
+ - 同目录下允许同 file_name 并存(每次上传生成新的 path),不会因重名失败。
110
+ - 下载时 src 必须是完整 path(裸 file_name 会被识别为本地路径)。
111
+ - 上传成功返回 download_url,可写入数据库 / 应用代码作为永久引用。
112
+
113
+ Examples:
114
+ # 上传:本地 → 远程
115
+ $ miaoda file cp ./logo.png /images/brand/
116
+ ✓ Uploaded logo.png → /images/brand/1858537546760216.png
117
+ file_name: logo.png
118
+ path: /images/brand/1858537546760216.png
119
+ size: 24 KB (24580 bytes)
120
+ type: image/png
121
+ download_url: /spark/app/.../1858537546760216.png
122
+
123
+ # 下载:远程 → 本地
124
+ $ miaoda file cp /images/brand/1858537546760216.png ~/Desktop/
125
+ ✓ Downloaded 1858537546760216.png → ~/Desktop/logo.png (24 KB)
126
+
127
+ # 上传时改名
128
+ $ miaoda file cp ./photo.png /images/ --rename avatar.png
129
+ ✓ Uploaded photo.png → /images/1858537546760301.png
130
+
131
+ # 报错:本地源文件不存在
132
+ $ miaoda file cp ./missing.png /images/
133
+ Error: Local file './missing.png' does not exist
134
+
135
+ # 报错:超过上传上限
136
+ $ miaoda file cp ./video.mp4 /videos/
137
+ Error: File size 230 MB exceeds the 100 MB upload limit
138
+ hint: Split the file, or use the web console for large uploads.
139
+ `);
140
+ fileCmd
141
+ .command("rm")
142
+ .summary("删除一个或多个文件")
143
+ .description("删除一个或多个文件。<file> 可传 path(推荐)或 file_name,可混用。\n" +
144
+ "操作不可撤销(不进回收站),TTY 下默认会要求二次确认。")
145
+ .usage("[paths...] [flags]")
146
+ .argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
147
+ .option("-n, --name <name>", "按文件名删除(可重复指定)", (value, prev) => [...(prev ?? []), value])
148
+ .option("-y, --yes", "跳过交互确认;非交互场景必加")
149
+ .action(async (paths, opts) => {
150
+ await (0, index_1.handleFileRm)(paths, opts);
151
+ })
152
+ .addHelpText("after", `
153
+ Notes:
154
+ - 删除不可撤销,没有回收站。
155
+ - 批量场景下失败项不影响其他项;全部成功退出码 0,任意一项失败退出码 1。
156
+ - --json 输出每项保留输入顺序,含 status: "ok" | "error" 字段。
157
+
158
+ Examples:
159
+ # 单文件删除(TTY 下需确认)
160
+ $ miaoda file rm /images/brand/1858537546760216.png
161
+ ? Delete '/images/brand/1858537546760216.png'? (y/N) y
162
+ ✓ Deleted /images/brand/1858537546760216.png
163
+
164
+ # 批量删除(混用 path 与 file_name)
165
+ $ miaoda file rm /images/A.png /images/B.png /docs/C.pdf --yes
166
+ ✓ Deleted /images/A.png
167
+ ✓ Deleted /images/B.png
168
+ ✓ Deleted /docs/C.pdf
169
+ Deleted 3 of 3 files
170
+
171
+ # 部分失败(其他项仍会被删除)
172
+ $ miaoda file rm logo.png missing.png report.pdf --yes
173
+ ✓ Deleted logo.png
174
+ ✗ missing.png: File does not exist
175
+ ✓ Deleted report.pdf
176
+ Deleted 2 of 3 files (1 failed)
177
+
178
+ # 报错:file_name 多匹配
179
+ $ miaoda file rm logo.png --yes
180
+ Error: Multiple files match name 'logo.png' (2 found)
181
+ hint: Use path instead. Run \`miaoda file ls --name logo.png\` to see candidates.
182
+ `);
183
+ fileCmd
184
+ .command("sign")
185
+ .summary("生成公网可访问的临时下载链接")
186
+ .description("生成 signed_url——公网可直接访问的临时下载链接,带过期时间。\n" +
187
+ "仅在需要浏览器打开 / 公网分享时使用;应用代码内引用文件请用 download_url\n" +
188
+ "(来自 cp 或 stat),无需 sign。")
189
+ .usage("<file> [flags]")
190
+ .argument("<file>", "文件的路径或文件名")
191
+ .option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 等单位(默认 1d,最长 30d)")
192
+ .action(async (file, opts) => {
193
+ await (0, index_1.handleFileSign)(file, opts);
194
+ })
195
+ .addHelpText("after", `
196
+ Notes:
197
+ - <file> 可传 path(推荐)或 file_name;重名报 AMBIGUOUS_FILE_NAME。
198
+ - signed_url 会过期,不要持久化到数据库。永久引用请用 download_url。
199
+
200
+ Examples:
201
+ $ miaoda file sign /images/brand/1858537546760216.png
202
+ https://miaoda.feishu.cn/storage/.../1858537546760216.png?token=xxx&expires=86400
203
+
204
+ $ miaoda file sign /images/brand/1858537546760216.png --expires 30d
205
+ https://miaoda.feishu.cn/storage/.../1858537546760216.png?token=xxx&expires=2592000
206
+
207
+ # 报错:过期时间超过上限
208
+ $ miaoda file sign /images/brand/1858537546760216.png --expires 60d
209
+ Error: Expires duration '60d' exceeds the maximum of 30d
210
+ hint: Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.
211
+ `);
212
+ }
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerCommands = registerCommands;
4
4
  const index_1 = require("../../cli/commands/plugin/index");
5
+ const index_2 = require("../../cli/commands/file/index");
6
+ const index_3 = require("../../cli/commands/db/index");
5
7
  function registerCommands(program) {
6
8
  (0, index_1.registerPluginCommands)(program);
9
+ (0, index_2.registerFileCommands)(program);
10
+ (0, index_3.registerDbCommands)(program);
7
11
  }
@@ -5,7 +5,8 @@ const index_1 = require("../../../cli/handlers/plugin/index");
5
5
  function registerPluginCommands(program) {
6
6
  const pluginCmd = program
7
7
  .command("plugin")
8
- .description("插件管理:安装/更新/移除插件包,查询 capability 实例");
8
+ .description("插件管理:安装/更新/移除插件包,查询 capability 实例")
9
+ .usage("<command> [flags]");
9
10
  pluginCmd.action(() => {
10
11
  pluginCmd.outputHelp();
11
12
  });
@@ -1,29 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appIdOption = appIdOption;
4
3
  exports.softRequiredOption = softRequiredOption;
5
4
  exports.resolveAppId = resolveAppId;
6
5
  exports.withHelp = withHelp;
7
6
  exports.failArgs = failArgs;
8
7
  const commander_1 = require("commander");
9
8
  const error_1 = require("../../utils/error");
10
- /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption()) */
11
- function appIdOption() {
12
- return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
13
- }
14
9
  /**
15
10
  * soft-required: Commander 类型上 optional,runtime 校验必填。
16
11
  */
17
12
  function softRequiredOption(name, desc) {
18
13
  return new commander_1.Option(name, desc);
19
14
  }
20
- /** 解析 appId,CLI flag > env > 抛错 */
15
+ /**
16
+ * 解析 appId,从环境变量 MIAODA_APP_ID 或 app_id(小写下划线,部分外部沙箱注入)读取。
17
+ * 缺失时抛 APP_ID_MISSING,由全局 catch 处理。
18
+ *
19
+ * opts 里仍保留 appId(可选),用于测试 / 高级场景显式注入;正常 CLI 不暴露此 flag。
20
+ */
21
21
  function resolveAppId(opts) {
22
- const id = opts.appId ?? process.env.MIAODA_APP_ID;
22
+ const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
23
23
  if (!id) {
24
24
  throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
25
25
  next_actions: [
26
- "传入 --app-id <id>",
27
26
  "设置 export MIAODA_APP_ID=<id>",
28
27
  ],
29
28
  });
@@ -0,0 +1,183 @@
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.handleDbDataImport = handleDbDataImport;
37
+ exports.handleDbDataExport = handleDbDataExport;
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const path = __importStar(require("node:path"));
40
+ const api = __importStar(require("../../../api/index"));
41
+ const error_1 = require("../../../utils/error");
42
+ const output_1 = require("../../../utils/output");
43
+ const shared_1 = require("../../../cli/commands/shared");
44
+ const render_1 = require("../../../utils/render");
45
+ // P0 规格(对齐技术方案关键决策 2)
46
+ const MAX_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
47
+ const MAX_ROWS = 5000;
48
+ async function handleDbDataImport(file, opts) {
49
+ const appId = (0, shared_1.resolveAppId)(opts);
50
+ const ext = path.extname(file).toLowerCase();
51
+ const format = resolveFormat(opts.format, ext, "import");
52
+ let body;
53
+ try {
54
+ body = await fs.readFile(file);
55
+ }
56
+ catch (err) {
57
+ const code = err.code;
58
+ if (code === "ENOENT") {
59
+ throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, { next_actions: ["Check the file path."] });
60
+ }
61
+ throw err;
62
+ }
63
+ if (body.length > MAX_SIZE_BYTES) {
64
+ throw new error_1.AppError("IMPORT_SIZE_EXCEEDED", `Import exceeds 1 MB limit (file is ${String(body.length)} bytes)`, { next_actions: ["Split the file into chunks of ≤ 5000 rows / 1 MB and import separately."] });
65
+ }
66
+ const rowCount = countRows(body, format);
67
+ if (rowCount > MAX_ROWS) {
68
+ throw new error_1.AppError("IMPORT_ROWS_EXCEEDED", `Import exceeds 5000 rows limit (file has ${String(rowCount)} rows)`, { next_actions: ["Split the file into chunks of ≤ 5000 rows / 1 MB and import separately."] });
69
+ }
70
+ const tableName = opts.table ?? path.basename(file, ext);
71
+ if (!tableName) {
72
+ throw new error_1.AppError("ARGS_INVALID", "Cannot infer target table from file name; specify --table");
73
+ }
74
+ const result = await api.db.importData({
75
+ appId,
76
+ tableName,
77
+ format,
78
+ body,
79
+ });
80
+ if ((0, output_1.isJsonMode)()) {
81
+ (0, output_1.emit)({
82
+ data: {
83
+ file,
84
+ table: result.tableName,
85
+ rows: result.recordCount,
86
+ },
87
+ });
88
+ return;
89
+ }
90
+ const tty = (0, render_1.isStdoutTty)();
91
+ (0, output_1.emit)(tty
92
+ ? `✓ Imported ${file} → table '${result.tableName}' (${String(result.recordCount)} rows)`
93
+ : `OK Imported ${file} -> table '${result.tableName}' (${String(result.recordCount)} rows)`);
94
+ }
95
+ async function handleDbDataExport(table, opts) {
96
+ const appId = (0, shared_1.resolveAppId)(opts);
97
+ const format = resolveFormat(opts.format, undefined, "export", "csv");
98
+ const outputPath = opts.file ?? `${table}.${format}`;
99
+ const limit = opts.limit ? Number(opts.limit) : MAX_ROWS;
100
+ if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_ROWS) {
101
+ throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer ≤ ${String(MAX_ROWS)}`);
102
+ }
103
+ if (!opts.force) {
104
+ try {
105
+ await fs.access(outputPath);
106
+ throw new error_1.AppError("FILE_ALREADY_EXISTS", `Output file '${outputPath}' already exists`, { next_actions: ["Use -f to specify a different path, or --force to overwrite."] });
107
+ }
108
+ catch (err) {
109
+ if (err instanceof error_1.AppError)
110
+ throw err;
111
+ if (err.code !== "ENOENT")
112
+ throw err;
113
+ }
114
+ }
115
+ const result = await api.db.exportData({
116
+ appId,
117
+ tableName: table,
118
+ format,
119
+ limit,
120
+ });
121
+ if (result.body.length > MAX_SIZE_BYTES) {
122
+ throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, { next_actions: [`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`] });
123
+ }
124
+ await fs.writeFile(outputPath, result.body);
125
+ // 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
126
+ const rows = result.recordCount ?? countRows(result.body, format);
127
+ if ((0, output_1.isJsonMode)()) {
128
+ (0, output_1.emit)({
129
+ data: {
130
+ table,
131
+ file: outputPath,
132
+ format,
133
+ rows,
134
+ },
135
+ });
136
+ return;
137
+ }
138
+ const tty = (0, render_1.isStdoutTty)();
139
+ (0, output_1.emit)(tty
140
+ ? `✓ Exported ${table} → ${outputPath} (${String(rows)} rows)`
141
+ : `OK Exported ${table} -> ${outputPath} (${String(rows)} rows)`);
142
+ }
143
+ // ── 共用辅助 ──
144
+ function resolveFormat(explicit, ext, scope, fallback) {
145
+ const raw = (explicit ?? ext ?? fallback ?? "").replace(/^\./, "").toLowerCase();
146
+ if (raw === "csv")
147
+ return "csv";
148
+ if (raw === "json")
149
+ return "json";
150
+ // sql 仅 export 路径接受 —— import 端后端仍只支持 csv/json。
151
+ if (raw === "sql" && scope === "export")
152
+ return "sql";
153
+ const code = scope === "import" ? "IMPORT_FORMAT_UNSUPPORTED" : "EXPORT_FORMAT_UNSUPPORTED";
154
+ throw new error_1.AppError(code, `Unrecognized format '${raw || "(unspecified)"}'`, {
155
+ next_actions: [
156
+ scope === "import"
157
+ ? "Supported formats: .csv, .json. Convert the file first, or rename with the correct extension."
158
+ : "Supported formats: csv, json, sql. Pass --format csv|json|sql.",
159
+ ],
160
+ });
161
+ }
162
+ function countRows(body, format) {
163
+ if (format === "csv") {
164
+ // 粗估:非空行数 - 表头 1 行
165
+ let lines = 0;
166
+ const text = body.toString("utf8");
167
+ for (const line of text.split(/\r?\n/)) {
168
+ if (line.length > 0)
169
+ lines += 1;
170
+ }
171
+ return Math.max(0, lines - 1);
172
+ }
173
+ // JSON:期望是顶层数组
174
+ try {
175
+ const parsed = JSON.parse(body.toString("utf8"));
176
+ if (Array.isArray(parsed))
177
+ return parsed.length;
178
+ return 1;
179
+ }
180
+ catch {
181
+ return 0;
182
+ }
183
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleDbDataExport = exports.handleDbDataImport = exports.handleDbSchemaGet = exports.handleDbSchemaList = exports.handleDbSql = void 0;
4
+ var sql_1 = require("./sql");
5
+ Object.defineProperty(exports, "handleDbSql", { enumerable: true, get: function () { return sql_1.handleDbSql; } });
6
+ var schema_1 = require("./schema");
7
+ Object.defineProperty(exports, "handleDbSchemaList", { enumerable: true, get: function () { return schema_1.handleDbSchemaList; } });
8
+ Object.defineProperty(exports, "handleDbSchemaGet", { enumerable: true, get: function () { return schema_1.handleDbSchemaGet; } });
9
+ var data_1 = require("./data");
10
+ Object.defineProperty(exports, "handleDbDataImport", { enumerable: true, get: function () { return data_1.handleDbDataImport; } });
11
+ Object.defineProperty(exports, "handleDbDataExport", { enumerable: true, get: function () { return data_1.handleDbDataExport; } });
@@ -0,0 +1,174 @@
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.handleDbSchemaList = handleDbSchemaList;
37
+ exports.handleDbSchemaGet = handleDbSchemaGet;
38
+ const api = __importStar(require("../../../api/index"));
39
+ const error_1 = require("../../../utils/error");
40
+ const output_1 = require("../../../utils/output");
41
+ const render_1 = require("../../../utils/render");
42
+ const shared_1 = require("../../../cli/commands/shared");
43
+ const colors_1 = require("../../../utils/colors");
44
+ const index_1 = require("../../../api/db/index");
45
+ // ── schema list ──
46
+ async function handleDbSchemaList(opts) {
47
+ const appId = (0, shared_1.resolveAppId)(opts);
48
+ const resp = await api.db.getSchema({ appId, format: "schema", includeStats: true, dbBranch: opts.env });
49
+ const tables = (0, index_1.flattenSchemaList)(resp.schema);
50
+ if ((0, output_1.isJsonMode)()) {
51
+ (0, output_1.emit)({ data: tables });
52
+ return;
53
+ }
54
+ if (tables.length === 0) {
55
+ (0, output_1.emit)("No tables found.");
56
+ return;
57
+ }
58
+ const tty = (0, render_1.isStdoutTty)();
59
+ // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)。
60
+ // updated_at 暂时不展示——PG pg_catalog 不存真实表时间,详见 renderDetail 注释。
61
+ const headers = [
62
+ "name",
63
+ "description",
64
+ "estimated_row_count",
65
+ tty ? "size" : "size_bytes",
66
+ "columns",
67
+ ];
68
+ const rows = tables.map((t) => [
69
+ t.name,
70
+ t.description ?? "—",
71
+ t.estimated_row_count === null ? "—" : String(t.estimated_row_count),
72
+ t.size_bytes === null ? "—" : (tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes)),
73
+ String(t.columns),
74
+ ]);
75
+ (0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
76
+ }
77
+ async function handleDbSchemaGet(table, opts) {
78
+ const appId = (0, shared_1.resolveAppId)(opts);
79
+ const tty = (0, render_1.isStdoutTty)();
80
+ const forceDdl = Boolean(opts.ddl);
81
+ const wantsStructured = (0, output_1.isJsonMode)() || (tty && !forceDdl);
82
+ if (!wantsStructured) {
83
+ // non-TTY 或 --ddl:直接取完整 DDL 输出
84
+ const ddlResp = await api.db.getSchema({
85
+ appId,
86
+ format: "ddl",
87
+ tableNames: table,
88
+ dbBranch: opts.env,
89
+ });
90
+ const sql = ddlResp.ddl?.[table];
91
+ if (!sql) {
92
+ throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
93
+ next_actions: [
94
+ `Did you mean another table? Run "miaoda db schema list" to see all tables.`,
95
+ ],
96
+ });
97
+ }
98
+ (0, output_1.emit)(sql.trimEnd());
99
+ return;
100
+ }
101
+ // TTY / JSON:取结构化
102
+ const resp = await api.db.getSchema({
103
+ appId,
104
+ format: "schema",
105
+ tableNames: table,
106
+ includeStats: true,
107
+ dbBranch: opts.env,
108
+ });
109
+ const detail = (0, index_1.pickTableDetail)(resp.schema, table);
110
+ if (!detail) {
111
+ throw new error_1.AppError("TABLE_NOT_FOUND", `Table '${table}' does not exist`, {
112
+ next_actions: [
113
+ `Did you mean another table? Run "miaoda db schema list" to see all tables.`,
114
+ ],
115
+ });
116
+ }
117
+ if ((0, output_1.isJsonMode)()) {
118
+ (0, output_1.emitOk)(detail);
119
+ return;
120
+ }
121
+ (0, output_1.emit)(renderDetail(detail, tty));
122
+ }
123
+ function renderDetail(d, tty) {
124
+ const systemFields = d.columns.filter((c) => c.name.startsWith("_"));
125
+ const userFields = d.columns.filter((c) => !c.name.startsWith("_"));
126
+ // header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size。
127
+ // 不展示 Created / Updated:PG pg_catalog 不存表创建时间,dataloom 用 OID
128
+ // 构造的伪时间戳(baseTime=2020-01-01 + OID 秒偏移),仅保排序意义、绝对值
129
+ // 误导性强,先去掉。后续如果接 ddl_change_log 取真实时间再加回。
130
+ const header = [
131
+ // 表名做 cyan 强调(spec:spec 里的"命令名/表名"类强调值都走 highlight)
132
+ ["Name", tty ? colors_1.c.highlight(d.name) : d.name],
133
+ ["Description", d.description ?? "—"],
134
+ [
135
+ "Columns",
136
+ systemFields.length > 0
137
+ ? `${String(userFields.length)} (+ ${String(systemFields.length)} system)`
138
+ : String(userFields.length),
139
+ ],
140
+ ["Estimated Rows", d.estimated_row_count === null ? "—" : String(d.estimated_row_count)],
141
+ ["Size", d.size_bytes === null ? "—" : (0, render_1.formatSize)(d.size_bytes)],
142
+ ];
143
+ const colHeaders = ["column", "type", "nullable", "default", "comment"];
144
+ const colRows = userFields.map((c) => [
145
+ c.name,
146
+ c.type,
147
+ c.nullable ? "yes" : "no",
148
+ c.default ?? "—",
149
+ c.comment ?? "—",
150
+ ]);
151
+ const parts = [];
152
+ parts.push((0, render_1.renderKeyValue)(header, tty));
153
+ parts.push("");
154
+ parts.push(tty ? (0, render_1.renderAlignedTable)(colHeaders, colRows) : (0, render_1.renderTsv)(colHeaders, colRows));
155
+ // Constraints 段(PRIMARY KEY / UNIQUE):表级约束独立成段,与普通索引分离展示
156
+ if (d.constraints.length > 0) {
157
+ parts.push("");
158
+ parts.push(tty ? " Constraints:" : "Constraints:");
159
+ for (const c of d.constraints) {
160
+ const line = `${c.type} (${c.columns.join(", ")})`;
161
+ parts.push(tty ? ` ${line}` : line);
162
+ }
163
+ }
164
+ // Indexes 段:普通索引,格式 "<name> ON <col1, col2> USING <method>"
165
+ if (d.indexes.length > 0) {
166
+ parts.push("");
167
+ parts.push(tty ? " Indexes:" : "Indexes:");
168
+ for (const idx of d.indexes) {
169
+ const line = `${idx.name} ON ${idx.columns.join(", ")} USING ${idx.method}`;
170
+ parts.push(tty ? ` ${line}` : line);
171
+ }
172
+ }
173
+ return parts.join("\n");
174
+ }