@lark-apaas/miaoda-cli 0.1.0 → 0.1.1-alpha.07b4fd5
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/db/api.js +278 -0
- package/dist/api/db/client.js +169 -0
- package/dist/api/db/index.js +16 -0
- package/dist/api/db/parsers.js +174 -0
- package/dist/api/db/sql-keywords.js +52 -0
- package/dist/api/db/types.js +10 -0
- package/dist/api/file/api.js +509 -0
- package/dist/api/file/client.js +199 -0
- package/dist/api/file/detect.js +56 -0
- package/dist/api/file/index.js +18 -0
- package/dist/api/file/parsers.js +72 -0
- package/dist/api/file/types.js +3 -0
- package/dist/api/index.js +5 -1
- package/dist/api/plugin/api.js +3 -3
- package/dist/cli/commands/db/index.js +209 -0
- package/dist/cli/commands/file/index.js +212 -0
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +7 -8
- package/dist/cli/handlers/db/data.js +183 -0
- package/dist/cli/handlers/db/index.js +11 -0
- package/dist/cli/handlers/db/schema.js +174 -0
- package/dist/cli/handlers/db/sql.js +647 -0
- package/dist/cli/handlers/file/cp.js +221 -0
- package/dist/cli/handlers/file/index.js +13 -0
- package/dist/cli/handlers/file/ls.js +111 -0
- package/dist/cli/handlers/file/rm.js +264 -0
- package/dist/cli/handlers/file/sign.js +96 -0
- package/dist/cli/handlers/file/stat.js +97 -0
- package/dist/cli/handlers/index.js +2 -0
- package/dist/cli/help.js +188 -0
- package/dist/main.js +9 -1
- package/dist/utils/colors.js +98 -0
- package/dist/utils/error.js +14 -0
- package/dist/utils/fuzzy-match.js +91 -0
- package/dist/utils/http.js +31 -10
- package/dist/utils/index.js +3 -1
- package/dist/utils/output.js +75 -9
- package/dist/utils/render.js +192 -0
- package/package.json +4 -3
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.handleFileCp = handleFileCp;
|
|
40
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
43
|
+
const api = __importStar(require("../../../api/index"));
|
|
44
|
+
const output_1 = require("../../../utils/output");
|
|
45
|
+
const render_1 = require("../../../utils/render");
|
|
46
|
+
const error_1 = require("../../../utils/error");
|
|
47
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
48
|
+
const colors_1 = require("../../../utils/colors");
|
|
49
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
50
|
+
/**
|
|
51
|
+
* 判断 src 是本地文件还是远程引用:
|
|
52
|
+
* - src 本地 fs 可访问 → upload(dst 当 remote)
|
|
53
|
+
* - 其他 → download(src 是 /path 或 file_name,由 resolveRemotePath 解析)
|
|
54
|
+
*
|
|
55
|
+
* `cp` 语义要求 src 必须存在;本地不存在就认为用户指向远程。不需要给 dst 猜方向。
|
|
56
|
+
*/
|
|
57
|
+
function isLocalSrc(src) {
|
|
58
|
+
const expanded = expandHome(src);
|
|
59
|
+
try {
|
|
60
|
+
return node_fs_1.default.existsSync(expanded);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function expandHome(p) {
|
|
67
|
+
if (p.startsWith("~/"))
|
|
68
|
+
return node_path_1.default.join(node_os_1.default.homedir(), p.slice(2));
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
function detectMime(filePath) {
|
|
72
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
73
|
+
const map = {
|
|
74
|
+
".png": "image/png",
|
|
75
|
+
".jpg": "image/jpeg",
|
|
76
|
+
".jpeg": "image/jpeg",
|
|
77
|
+
".gif": "image/gif",
|
|
78
|
+
".webp": "image/webp",
|
|
79
|
+
".svg": "image/svg+xml",
|
|
80
|
+
".pdf": "application/pdf",
|
|
81
|
+
".json": "application/json",
|
|
82
|
+
".csv": "text/csv",
|
|
83
|
+
".txt": "text/plain",
|
|
84
|
+
".html": "text/html",
|
|
85
|
+
".zip": "application/zip",
|
|
86
|
+
".tar": "application/x-tar",
|
|
87
|
+
".gz": "application/gzip",
|
|
88
|
+
".mp4": "video/mp4",
|
|
89
|
+
".mp3": "audio/mpeg",
|
|
90
|
+
};
|
|
91
|
+
return map[ext] ?? "application/octet-stream";
|
|
92
|
+
}
|
|
93
|
+
/** 把 src 的远程形式(`/path` / `file_name`)解析成后端 sign 可用的 remote path。 */
|
|
94
|
+
async function resolveRemotePath(appId, input) {
|
|
95
|
+
if (input.startsWith("/"))
|
|
96
|
+
return input;
|
|
97
|
+
const [resolved] = await api.file.resolveInputs({ appId, inputs: [input] });
|
|
98
|
+
if (resolved.status === "error") {
|
|
99
|
+
throw new error_1.AppError(resolved.error.code, resolved.error.message, {
|
|
100
|
+
next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return resolved.file.path;
|
|
104
|
+
}
|
|
105
|
+
async function handleUpload(appId, localRaw, remoteRaw, rename) {
|
|
106
|
+
const localPath = expandHome(localRaw);
|
|
107
|
+
if (!node_fs_1.default.existsSync(localPath) || !node_fs_1.default.statSync(localPath).isFile()) {
|
|
108
|
+
throw new error_1.AppError("FILE_SRC_NOT_FOUND", `Local file '${localRaw}' does not exist`);
|
|
109
|
+
}
|
|
110
|
+
const stat = node_fs_1.default.statSync(localPath);
|
|
111
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
112
|
+
throw new error_1.AppError("FILE_SIZE_EXCEEDED", `File size ${(0, render_1.formatSize)(stat.size)} exceeds the 100 MB upload limit`, {
|
|
113
|
+
next_actions: [
|
|
114
|
+
"Split the file, or use the web console for large uploads.",
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const fileName = rename ?? node_path_1.default.basename(localPath);
|
|
119
|
+
// remoteRaw 直接透传给服务端,由服务端按"是否以 / 结尾"区分两种语义:
|
|
120
|
+
// - 以 / 结尾 → 目录前缀模式,服务端在前缀下生成 "<16位ID>.<扩展名>"
|
|
121
|
+
// 例:cp ./logo.png /images/ → /images/1858537546760216.png
|
|
122
|
+
// - 不以 / 结尾 → 完整对象 key 模式(旧语义,原样落库)
|
|
123
|
+
// 例:cp ./photo.jpg /uploads/photo.jpg → /uploads/photo.jpg
|
|
124
|
+
// CLI 不做任何自动改写,避免猜测用户意图导致的 path 形态不可预测。
|
|
125
|
+
const remotePath = remoteRaw;
|
|
126
|
+
const contentType = detectMime(localPath);
|
|
127
|
+
const result = await api.file.uploadFile({
|
|
128
|
+
appId,
|
|
129
|
+
localPath,
|
|
130
|
+
remotePath,
|
|
131
|
+
fileName,
|
|
132
|
+
fileSize: stat.size,
|
|
133
|
+
contentType,
|
|
134
|
+
readFile: async () => node_fs_1.default.promises.readFile(localPath),
|
|
135
|
+
});
|
|
136
|
+
if ((0, output_1.isJsonMode)()) {
|
|
137
|
+
const payload = {
|
|
138
|
+
file_name: result.file_name,
|
|
139
|
+
path: result.path,
|
|
140
|
+
size_bytes: result.size,
|
|
141
|
+
type: result.type,
|
|
142
|
+
};
|
|
143
|
+
if (result.download_url)
|
|
144
|
+
payload.download_url = result.download_url;
|
|
145
|
+
(0, output_1.emitOk)(payload);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
149
|
+
const lines = [];
|
|
150
|
+
if (tty) {
|
|
151
|
+
lines.push(colors_1.c.success(`✓ Uploaded ${node_path_1.default.basename(localPath)} → ${result.path}`));
|
|
152
|
+
lines.push(` file_name: ${result.file_name}`);
|
|
153
|
+
lines.push(` path: ${result.path}`);
|
|
154
|
+
lines.push(` size: ${(0, render_1.formatSize)(result.size)} (${String(result.size)} bytes)`);
|
|
155
|
+
lines.push(` type: ${result.type}`);
|
|
156
|
+
if (result.download_url) {
|
|
157
|
+
lines.push(` download_url: ${result.download_url}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
lines.push(`OK Uploaded ${node_path_1.default.basename(localPath)} -> ${result.path}`);
|
|
162
|
+
lines.push(`file_name\t${result.file_name}`);
|
|
163
|
+
lines.push(`path\t${result.path}`);
|
|
164
|
+
lines.push(`size\t${String(result.size)}`);
|
|
165
|
+
lines.push(`type\t${result.type}`);
|
|
166
|
+
if (result.download_url) {
|
|
167
|
+
lines.push(`download_url\t${result.download_url}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
(0, output_1.emit)(lines.join("\n"));
|
|
171
|
+
}
|
|
172
|
+
async function handleDownload(appId, remoteRaw, localRaw) {
|
|
173
|
+
const filePath = await resolveRemotePath(appId, remoteRaw);
|
|
174
|
+
const sign = await api.file.signDownload({
|
|
175
|
+
appId,
|
|
176
|
+
filePath,
|
|
177
|
+
expiresIn: 300,
|
|
178
|
+
});
|
|
179
|
+
const baseName = node_path_1.default.basename(filePath);
|
|
180
|
+
let localTarget = expandHome(localRaw);
|
|
181
|
+
if (localTarget === "./" || localTarget === "." || localTarget.endsWith("/")) {
|
|
182
|
+
localTarget = node_path_1.default.join(localTarget, baseName);
|
|
183
|
+
}
|
|
184
|
+
else if (node_fs_1.default.existsSync(localTarget) && node_fs_1.default.statSync(localTarget).isDirectory()) {
|
|
185
|
+
localTarget = node_path_1.default.join(localTarget, baseName);
|
|
186
|
+
}
|
|
187
|
+
let writtenBytes = 0;
|
|
188
|
+
await api.file.downloadFile({
|
|
189
|
+
signedURL: sign.signed_url,
|
|
190
|
+
writeFile: async (buf) => {
|
|
191
|
+
await node_fs_1.default.promises.mkdir(node_path_1.default.dirname(node_path_1.default.resolve(localTarget)), {
|
|
192
|
+
recursive: true,
|
|
193
|
+
});
|
|
194
|
+
await node_fs_1.default.promises.writeFile(localTarget, buf);
|
|
195
|
+
writtenBytes = buf.length;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
if ((0, output_1.isJsonMode)()) {
|
|
199
|
+
(0, output_1.emitOk)({
|
|
200
|
+
path: filePath,
|
|
201
|
+
local_path: localTarget,
|
|
202
|
+
size: writtenBytes,
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
207
|
+
if (tty) {
|
|
208
|
+
(0, output_1.emit)(colors_1.c.success(`✓ Downloaded ${baseName} → ${localTarget} (${(0, render_1.formatSize)(writtenBytes)})`));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
(0, output_1.emit)(`OK Downloaded ${baseName} -> ${localTarget} (${String(writtenBytes)} bytes)`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function handleFileCp(src, dst, opts) {
|
|
215
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
216
|
+
// src 本地存在 → upload;其他情况(file_ref / `/path` / 裸文件名)→ download
|
|
217
|
+
if (isLocalSrc(src)) {
|
|
218
|
+
return handleUpload(appId, src, dst, opts.rename);
|
|
219
|
+
}
|
|
220
|
+
return handleDownload(appId, src, dst);
|
|
221
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleFileSign = exports.handleFileRm = exports.handleFileCp = exports.handleFileStat = exports.handleFileLs = void 0;
|
|
4
|
+
var ls_1 = require("./ls");
|
|
5
|
+
Object.defineProperty(exports, "handleFileLs", { enumerable: true, get: function () { return ls_1.handleFileLs; } });
|
|
6
|
+
var stat_1 = require("./stat");
|
|
7
|
+
Object.defineProperty(exports, "handleFileStat", { enumerable: true, get: function () { return stat_1.handleFileStat; } });
|
|
8
|
+
var cp_1 = require("./cp");
|
|
9
|
+
Object.defineProperty(exports, "handleFileCp", { enumerable: true, get: function () { return cp_1.handleFileCp; } });
|
|
10
|
+
var rm_1 = require("./rm");
|
|
11
|
+
Object.defineProperty(exports, "handleFileRm", { enumerable: true, get: function () { return rm_1.handleFileRm; } });
|
|
12
|
+
var sign_1 = require("./sign");
|
|
13
|
+
Object.defineProperty(exports, "handleFileSign", { enumerable: true, get: function () { return sign_1.handleFileSign; } });
|
|
@@ -0,0 +1,111 @@
|
|
|
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.handleFileLs = handleFileLs;
|
|
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
|
+
* 把位置参数 `query` 路由到 `--path` 或 `--name`:
|
|
45
|
+
* - `looksLikePath(query)` → path 精确匹配
|
|
46
|
+
* - 否则 → file_name 精确匹配
|
|
47
|
+
*
|
|
48
|
+
* 显式 `--path` / `--name` 传入时跳过 query 自动识别。
|
|
49
|
+
* 同时传 query 又传 --path/--name 视为歧义,直接报错。
|
|
50
|
+
*/
|
|
51
|
+
function resolveQueryRouting(opts) {
|
|
52
|
+
const explicit = {
|
|
53
|
+
path: opts.path,
|
|
54
|
+
name: opts.name,
|
|
55
|
+
};
|
|
56
|
+
if (!opts.query)
|
|
57
|
+
return explicit;
|
|
58
|
+
if (explicit.path || explicit.name) {
|
|
59
|
+
throw new error_1.AppError("ARGS_INVALID", "Do not mix positional <query> with --path / --name; pick one.");
|
|
60
|
+
}
|
|
61
|
+
if ((0, index_1.looksLikePath)(opts.query)) {
|
|
62
|
+
// 补齐前导 `/`,对齐 ls sidecar 比较逻辑(info.path 永远带前导 `/`)
|
|
63
|
+
return { path: (0, index_1.toAbsolutePath)(opts.query) };
|
|
64
|
+
}
|
|
65
|
+
return { name: opts.query };
|
|
66
|
+
}
|
|
67
|
+
async function handleFileLs(opts) {
|
|
68
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
69
|
+
// commander 已经把 --limit 解析为 number;保留 ?? undefined 兼容老调用
|
|
70
|
+
const limit = opts.limit;
|
|
71
|
+
const sizeGt = opts.sizeGt ? (0, render_1.parseSize)(opts.sizeGt) : undefined;
|
|
72
|
+
const sizeLt = opts.sizeLt ? (0, render_1.parseSize)(opts.sizeLt) : undefined;
|
|
73
|
+
const { path, name } = resolveQueryRouting(opts);
|
|
74
|
+
const result = await api.file.listFiles({
|
|
75
|
+
appId,
|
|
76
|
+
limit,
|
|
77
|
+
cursor: opts.cursor,
|
|
78
|
+
all: opts.all,
|
|
79
|
+
path,
|
|
80
|
+
name,
|
|
81
|
+
type: opts.type,
|
|
82
|
+
sizeGt,
|
|
83
|
+
sizeLt,
|
|
84
|
+
uploadedSince: opts.uploadedSince,
|
|
85
|
+
uploadedUntil: opts.uploadedUntil,
|
|
86
|
+
});
|
|
87
|
+
if ((0, output_1.isJsonMode)()) {
|
|
88
|
+
(0, output_1.emitPaged)(result.items, result.next_cursor, result.has_more);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (result.items.length === 0) {
|
|
92
|
+
(0, output_1.emit)("No files found.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
96
|
+
const headers = ["file_name", "path", "size", "type", "uploaded_at"];
|
|
97
|
+
const rows = result.items.map((info) => [
|
|
98
|
+
info.file_name || "—",
|
|
99
|
+
info.path,
|
|
100
|
+
tty ? (0, render_1.formatSize)(info.size_bytes) : String(info.size_bytes),
|
|
101
|
+
info.type,
|
|
102
|
+
(0, render_1.formatTime)(info.uploaded_at, tty),
|
|
103
|
+
]);
|
|
104
|
+
const table = tty
|
|
105
|
+
? (0, render_1.renderAlignedTable)(headers, rows)
|
|
106
|
+
: (0, render_1.renderTsv)(headers, rows);
|
|
107
|
+
const hint = result.has_more && result.next_cursor
|
|
108
|
+
? `\n— ${String(result.items.length)} results. Next: --cursor ${result.next_cursor}`
|
|
109
|
+
: "";
|
|
110
|
+
(0, output_1.emit)(table + hint);
|
|
111
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.handleFileRm = handleFileRm;
|
|
40
|
+
const api = __importStar(require("../../../api/index"));
|
|
41
|
+
const output_1 = require("../../../utils/output");
|
|
42
|
+
const error_1 = require("../../../utils/error");
|
|
43
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
44
|
+
const index_1 = require("../../../api/file/index");
|
|
45
|
+
const render_1 = require("../../../utils/render");
|
|
46
|
+
const colors_1 = require("../../../utils/colors");
|
|
47
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
48
|
+
const MAX_BATCH = 100;
|
|
49
|
+
/**
|
|
50
|
+
* 解析位置参数(自动识别 path / file_name)与 `--name`(强制 file_name)两类输入。
|
|
51
|
+
*
|
|
52
|
+
* 位置参数:
|
|
53
|
+
* - `looksLikePath(p)` 为真(`/` 开头 / 含 `/` / 16+ 位 fileKey 模式)→ 视为 path,直接删
|
|
54
|
+
* - 否则视为 file_name → 走 FilterExpression 精确查找,命中唯一后再删
|
|
55
|
+
*
|
|
56
|
+
* `--name` 选项:
|
|
57
|
+
* - 强制按 file_name 处理,跳过 looksLikePath 判断
|
|
58
|
+
* - 走 FilterExpression `name eq X` 查找;0 命中 → FILE_NOT_FOUND;多匹配 → AMBIGUOUS_FILE_NAME
|
|
59
|
+
*/
|
|
60
|
+
async function resolveDeleteInputs(appId, paths, names) {
|
|
61
|
+
const resolved = [];
|
|
62
|
+
const errors = [];
|
|
63
|
+
// 1. 位置参数:按 looksLikePath 分流
|
|
64
|
+
const positionalAsPath = [];
|
|
65
|
+
const positionalAsName = [];
|
|
66
|
+
for (const p of paths) {
|
|
67
|
+
if ((0, index_1.looksLikePath)(p)) {
|
|
68
|
+
positionalAsPath.push(p);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
positionalAsName.push(p);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 1a. path 分支:直接进删除队列,file_name 取 basename 先占位
|
|
75
|
+
for (const p of positionalAsPath) {
|
|
76
|
+
const abs = (0, index_1.toAbsolutePath)(p);
|
|
77
|
+
const basename = abs.split("/").pop() ?? abs;
|
|
78
|
+
resolved.push({ input: p, path: abs, file_name: basename });
|
|
79
|
+
}
|
|
80
|
+
// 1b. name 分支(来自位置参数的非 path):和 --name 合并走 resolveInputs
|
|
81
|
+
const allNames = [...positionalAsName, ...names];
|
|
82
|
+
if (allNames.length > 0) {
|
|
83
|
+
const results = await api.file.resolveInputs({
|
|
84
|
+
appId,
|
|
85
|
+
inputs: allNames,
|
|
86
|
+
forceAs: "name",
|
|
87
|
+
});
|
|
88
|
+
for (const r of results) {
|
|
89
|
+
if (r.status === "ok") {
|
|
90
|
+
resolved.push({
|
|
91
|
+
input: r.input,
|
|
92
|
+
path: r.file.path,
|
|
93
|
+
file_name: r.file.file_name,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
errors.push({
|
|
98
|
+
status: "error",
|
|
99
|
+
input: r.input,
|
|
100
|
+
error: r.error,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { resolved, errors };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 删除前 TTY 二次确认。
|
|
109
|
+
* PRD 单文件场景下提示带具体路径:`? Delete '/path'? (y/N)`,
|
|
110
|
+
* 多文件场景汇总条数:`? Delete N files? (y/N)`。
|
|
111
|
+
* `firstInput` 是用户传入的第一个值(可能是 path 或 file_name),
|
|
112
|
+
* 单文件时直接展示给用户,方便核对目标。
|
|
113
|
+
*/
|
|
114
|
+
async function confirm(count, firstInput) {
|
|
115
|
+
const rl = node_readline_1.default.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stderr,
|
|
118
|
+
});
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const prompt = count === 1 ? `? Delete '${firstInput}'? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
|
|
121
|
+
rl.question(prompt, (answer) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function handleFileRm(paths, opts) {
|
|
128
|
+
const names = opts.name ?? [];
|
|
129
|
+
const totalCount = paths.length + names.length;
|
|
130
|
+
if (totalCount === 0) {
|
|
131
|
+
throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
|
|
132
|
+
}
|
|
133
|
+
if (totalCount > MAX_BATCH) {
|
|
134
|
+
throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
|
|
135
|
+
}
|
|
136
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
137
|
+
// destructive guardrail
|
|
138
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
139
|
+
if (tty && !opts.yes) {
|
|
140
|
+
// 单文件提示带具体目标方便用户核对;多文件传第一个 input 占位(实际只显示 N files)
|
|
141
|
+
// 上面已校验 totalCount > 0,paths/names 至少有一个非空
|
|
142
|
+
const firstInput = paths.length > 0 ? paths[0] : names[0];
|
|
143
|
+
const ok = await confirm(totalCount, firstInput);
|
|
144
|
+
if (!ok) {
|
|
145
|
+
throw new error_1.AppError("DESTRUCTIVE_CANCELLED", "Cancelled by user");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (!tty && !opts.yes) {
|
|
149
|
+
throw new error_1.AppError("DESTRUCTIVE_REQUIRES_CONFIRM", "This operation is destructive. Rerun with --yes to confirm.");
|
|
150
|
+
}
|
|
151
|
+
const { resolved, errors } = await resolveDeleteInputs(appId, paths, names);
|
|
152
|
+
// path → entry 映射(用于 delete 响应归档回原始 input 和 file_name)
|
|
153
|
+
const pathToEntry = new Map();
|
|
154
|
+
for (const e of resolved) {
|
|
155
|
+
pathToEntry.set(e.path, e);
|
|
156
|
+
}
|
|
157
|
+
const uniquePaths = Array.from(new Set(resolved.map((e) => e.path)));
|
|
158
|
+
let results = [...errors];
|
|
159
|
+
if (uniquePaths.length > 0) {
|
|
160
|
+
// 后端 delete 幂等语义,只返成功删除的 attachments。对比请求 vs 响应推断失败项;
|
|
161
|
+
// 失败项再发一次 head 探测具体原因(文件不存在 / 服务端错误)。
|
|
162
|
+
// TODO(storage): 推动后端 InnerBatchDeleteAttachmentsResponse 增加 failed 字段,
|
|
163
|
+
// 避免靠 HEAD 二次探测(多 N 次请求,仅失败场景才触发)。
|
|
164
|
+
const { deleted, failed } = await api.file.deleteFiles({
|
|
165
|
+
appId,
|
|
166
|
+
filePaths: uniquePaths,
|
|
167
|
+
});
|
|
168
|
+
for (const p of deleted) {
|
|
169
|
+
const entry = pathToEntry.get(p);
|
|
170
|
+
results.push({
|
|
171
|
+
status: "ok",
|
|
172
|
+
input: entry?.input ?? p,
|
|
173
|
+
file_name: entry?.file_name ?? (p.split("/").pop() ?? p),
|
|
174
|
+
path: p,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// 失败项:并发 HEAD 探测分类
|
|
178
|
+
const classified = await Promise.all(failed.map(async (f) => {
|
|
179
|
+
const entry = pathToEntry.get(f.path);
|
|
180
|
+
const input = entry?.input ?? f.path;
|
|
181
|
+
try {
|
|
182
|
+
await api.file.statFile({ appId, filePath: f.path });
|
|
183
|
+
return {
|
|
184
|
+
input,
|
|
185
|
+
code: "INTERNAL_ERROR",
|
|
186
|
+
message: "Delete request returned success but file still exists (server-side issue)",
|
|
187
|
+
hint: undefined,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof error_1.AppError && err.code === "FILE_NOT_FOUND") {
|
|
192
|
+
return {
|
|
193
|
+
input,
|
|
194
|
+
code: "FILE_NOT_FOUND",
|
|
195
|
+
message: `File '${input}' does not exist at delete time`,
|
|
196
|
+
hint: "Run `miaoda file ls` to see available files.",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
input,
|
|
201
|
+
code: "INTERNAL_ERROR",
|
|
202
|
+
message: err instanceof Error ? err.message : "verification failed",
|
|
203
|
+
hint: undefined,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
for (const c of classified) {
|
|
208
|
+
results.push({
|
|
209
|
+
status: "error",
|
|
210
|
+
input: c.input,
|
|
211
|
+
error: c.hint
|
|
212
|
+
? { code: c.code, message: c.message, hint: c.hint }
|
|
213
|
+
: { code: c.code, message: c.message },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// 按输入顺序重排(paths 优先,然后 names)
|
|
218
|
+
const orderIndex = new Map();
|
|
219
|
+
paths.forEach((p, i) => orderIndex.set(p, i));
|
|
220
|
+
names.forEach((n, i) => orderIndex.set(n, paths.length + i));
|
|
221
|
+
results = results.sort((a, b) => (orderIndex.get(a.input) ?? Number.MAX_SAFE_INTEGER) -
|
|
222
|
+
(orderIndex.get(b.input) ?? Number.MAX_SAFE_INTEGER));
|
|
223
|
+
const okCount = results.filter((r) => r.status === "ok").length;
|
|
224
|
+
const failCount = results.length - okCount;
|
|
225
|
+
if ((0, output_1.isJsonMode)()) {
|
|
226
|
+
(0, output_1.emit)({ data: results });
|
|
227
|
+
}
|
|
228
|
+
else if (tty) {
|
|
229
|
+
const lines = [];
|
|
230
|
+
for (const r of results) {
|
|
231
|
+
if (r.status === "ok")
|
|
232
|
+
lines.push(colors_1.c.success(`✓ Deleted ${r.input}`));
|
|
233
|
+
else
|
|
234
|
+
lines.push(colors_1.c.fail(`✗ ${r.input}: ${r.error.message}`));
|
|
235
|
+
}
|
|
236
|
+
if (failCount === 0) {
|
|
237
|
+
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
|
|
241
|
+
}
|
|
242
|
+
(0, output_1.emit)(lines.join("\n"));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const lines = [];
|
|
246
|
+
for (const r of results) {
|
|
247
|
+
if (r.status === "ok")
|
|
248
|
+
lines.push(`OK\t${r.input}`);
|
|
249
|
+
else
|
|
250
|
+
lines.push(`FAIL\t${r.input}\t${r.error.message}`);
|
|
251
|
+
}
|
|
252
|
+
if (failCount === 0) {
|
|
253
|
+
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
|
|
257
|
+
}
|
|
258
|
+
(0, output_1.emit)(lines.join("\n"));
|
|
259
|
+
}
|
|
260
|
+
// 退出码:任一失败 → 1;全成功 → 0
|
|
261
|
+
if (failCount > 0) {
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|